- 모든 HTML 파일에서 cdn.tailwindcss.com 대신 /lib/css/tailwind.min.css 사용 - 중복되는 인라인 스타일을 /css/common.css로 통합 - 외부 네트워크 의존성 제거로 페이지 로딩 지연 해결 변경된 파일: - DashBoard/index.html - Todo/index.html - Jobreport/index.html - Kuntae/index.html - Common.html - login.html - Project/index.html 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
703 lines
35 KiB
HTML
703 lines
35 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
|
<meta http-equiv="Pragma" content="no-cache">
|
|
<meta http-equiv="Expires" content="0">
|
|
<title>근태입력 조회</title>
|
|
<link href="/lib/css/tailwind.min.css" rel="stylesheet">
|
|
<link href="/css/common.css" rel="stylesheet">
|
|
<style>
|
|
|
|
.loading {
|
|
display: inline-block;
|
|
width: 20px;
|
|
height: 20px;
|
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
|
border-top: 3px solid #ffffff;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.table-container {
|
|
max-height: 600px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* 스크롤바 스타일링 */
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
width: var(--scrollbar-width, 16px);
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-track {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
background: rgba(255, 255, 255, 0.3);
|
|
border-radius: 8px;
|
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(255, 255, 255, 0.5);
|
|
}
|
|
</style>
|
|
</head>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 검색 및 필터 섹션 -->
|
|
<div class="glass-effect rounded-lg p-6 mb-6 animate-slide-up">
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label for="startDate" class="block text-sm font-medium text-white/80 mb-2">시작일</label>
|
|
<input type="date" id="startDate" class="w-full px-3 py-2 bg-white/20 border border-white/30 rounded-md text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent">
|
|
</div>
|
|
<div>
|
|
<label for="endDate" class="block text-sm font-medium text-white/80 mb-2">종료일</label>
|
|
<input type="date" id="endDate" class="w-full px-3 py-2 bg-white/20 border border-white/30 rounded-md text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent">
|
|
</div>
|
|
<div class="flex items-end">
|
|
<button id="searchBtn" class="w-full glass-effect text-white px-4 py-2 rounded-md hover:bg-white/30 transition-colors duration-200 flex items-center justify-center">
|
|
<span id="searchBtnText">조회</span>
|
|
<div id="searchBtnLoading" class="loading ml-2 hidden"></div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 통계 카드 -->
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6 animate-slide-up">
|
|
<div class="glass-effect rounded-lg p-6 card-hover">
|
|
<div class="flex items-center">
|
|
<div class="p-3 bg-primary-500/20 rounded-lg">
|
|
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-white/80">휴가사용</p>
|
|
<p id="totalDays" class="text-2xl font-bold text-white">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="glass-effect rounded-lg p-6 card-hover">
|
|
<div class="flex items-center">
|
|
<div class="p-3 bg-success-500/20 rounded-lg">
|
|
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-white/80">대체사용</p>
|
|
<p id="normalDays" class="text-2xl font-bold text-white">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="glass-effect rounded-lg p-6 card-hover">
|
|
<div class="flex items-center">
|
|
<div class="p-3 bg-warning-500/20 rounded-lg">
|
|
<svg class="w-6 h-6 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>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-white/80">잔량(년차)</p>
|
|
<p id="lateDays" class="text-2xl font-bold text-white">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="glass-effect rounded-lg p-6 card-hover">
|
|
<div class="flex items-center">
|
|
<div class="p-3 bg-danger-500/20 rounded-lg">
|
|
<svg class="w-6 h-6 text-white" 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>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-white/80">잔량(대체)</p>
|
|
<p id="absentDays" class="text-2xl font-bold text-white">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 데이터 테이블 -->
|
|
<div class="glass-effect rounded-lg animate-slide-up custom-scrollbar">
|
|
<div class="px-6 py-4 border-b border-white/20 flex justify-between items-center">
|
|
<h3 class="text-lg font-medium text-white">근태 상세 내역</h3>
|
|
<button id="addBtn" class="glass-effect text-white px-4 py-2 rounded-md hover:bg-white/30 transition-colors duration-200 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 class="table-container">
|
|
<table class="min-w-full divide-y divide-white/20">
|
|
<thead class="bg-white/10 sticky top-0">
|
|
<tr>
|
|
<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">시작일</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">사번</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">사용(일)</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">사용(H)</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">발생(H)</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">#</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">잔량(H)</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">전일(H)</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">등록자</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">등록일</th>
|
|
|
|
|
|
</tr>
|
|
</thead>
|
|
<tbody id="dataTableBody" class="divide-y divide-white/10">
|
|
<tr id="loadingRow" class="hidden">
|
|
<td colspan="18" class="px-6 py-4 text-center">
|
|
<div class="flex items-center justify-center">
|
|
<div class="loading mr-2"></div>
|
|
<span class="text-white/80">데이터를 불러오는 중...</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr id="noDataRow" class="hidden">
|
|
<td colspan="18" class="px-6 py-4 text-center text-white/70">
|
|
조회된 데이터가 없습니다.
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 근태 추가/편집 모달 -->
|
|
<div id="kuntaeModal" 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-96 shadow-lg rounded-md bg-white">
|
|
<div class="mt-3">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 id="modalTitle" class="text-lg font-medium text-gray-900">근태 추가</h3>
|
|
<button id="closeModal" class="text-gray-400 hover:text-gray-600">
|
|
<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>
|
|
|
|
<form id="kuntaeForm">
|
|
<input type="hidden" id="editId" name="id">
|
|
|
|
<div class="mb-4">
|
|
<label for="modalDate" class="block text-sm font-medium text-gray-700 mb-2">날짜</label>
|
|
<input type="date" id="modalDate" name="pdate" required
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<label for="modalInTime" class="block text-sm font-medium text-gray-700 mb-2">출근시간</label>
|
|
<input type="time" id="modalInTime" name="intime"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
|
</div>
|
|
<div>
|
|
<label for="modalOutTime" class="block text-sm font-medium text-gray-700 mb-2">퇴근시간</label>
|
|
<input type="time" id="modalOutTime" name="outtime"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="modalMemo" class="block text-sm font-medium text-gray-700 mb-2">비고</label>
|
|
<textarea id="modalMemo" name="memo" rows="3"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
|
placeholder="비고사항을 입력하세요"></textarea>
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-3">
|
|
<button type="button" id="cancelBtn"
|
|
class="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 transition-colors duration-200">
|
|
취소
|
|
</button>
|
|
<button type="submit" id="saveBtn"
|
|
class="px-4 py-2 bg-primary text-white rounded-md hover:bg-blue-600 transition-colors duration-200">
|
|
저장
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 삭제 확인 모달 -->
|
|
<div id="deleteModal" 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-96 shadow-lg rounded-md bg-white">
|
|
<div class="mt-3">
|
|
<div class="flex items-center mb-4">
|
|
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
|
|
<svg class="h-6 w-6 text-red-600" 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>
|
|
</div>
|
|
<div class="text-center">
|
|
<h3 class="text-lg font-medium text-gray-900 mb-2">근태 삭제</h3>
|
|
<p class="text-sm text-gray-500 mb-4">선택한 근태 데이터를 삭제하시겠습니까?</p>
|
|
<p id="deleteConfirmText" class="text-sm text-gray-700 mb-6"></p>
|
|
<div class="flex justify-center space-x-3">
|
|
<button id="cancelDeleteBtn"
|
|
class="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 transition-colors duration-200">
|
|
취소
|
|
</button>
|
|
<button id="confirmDeleteBtn"
|
|
class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors duration-200">
|
|
삭제
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 공통 네비게이션 -->
|
|
<script src="/js/common-navigation.js"></script>
|
|
|
|
<script>
|
|
// 전역 변수
|
|
let currentData = [];
|
|
let currentEditId = null;
|
|
|
|
// 비동기 프록시 캐싱 (한 번만 초기화)
|
|
const machine = window.chrome.webview.hostObjects.machine;
|
|
|
|
// 페이지 로드 시 초기화
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// 네비게이션 초기화
|
|
initNavigation('kuntae');
|
|
initializeDates();
|
|
loadData();
|
|
setupEventListeners();
|
|
setupModalEvents();
|
|
});
|
|
|
|
// 날짜 초기화 (현재 월)
|
|
function initializeDates() {
|
|
const now = new Date();
|
|
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
|
|
|
document.getElementById('startDate').value = startOfMonth.toISOString().split('T')[0];
|
|
document.getElementById('endDate').value = endOfMonth.toISOString().split('T')[0];
|
|
}
|
|
|
|
// 이벤트 리스너 설정
|
|
function setupEventListeners() {
|
|
document.getElementById('searchBtn').addEventListener('click', loadData);
|
|
document.getElementById('addBtn').addEventListener('click', showAddModal);
|
|
|
|
// Enter 키로 검색
|
|
document.getElementById('startDate').addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') loadData();
|
|
});
|
|
document.getElementById('endDate').addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') loadData();
|
|
});
|
|
}
|
|
|
|
// 모달 이벤트 설정
|
|
function setupModalEvents() {
|
|
// 근태 모달 이벤트
|
|
document.getElementById('closeModal').addEventListener('click', hideKuntaeModal);
|
|
document.getElementById('cancelBtn').addEventListener('click', hideKuntaeModal);
|
|
document.getElementById('kuntaeForm').addEventListener('submit', saveKuntae);
|
|
|
|
// 삭제 모달 이벤트
|
|
document.getElementById('cancelDeleteBtn').addEventListener('click', hideDeleteModal);
|
|
document.getElementById('confirmDeleteBtn').addEventListener('click', confirmDelete);
|
|
|
|
// 모달 외부 클릭 시 닫기
|
|
document.getElementById('kuntaeModal').addEventListener('click', function(e) {
|
|
if (e.target === this) hideKuntaeModal();
|
|
});
|
|
document.getElementById('deleteModal').addEventListener('click', function(e) {
|
|
if (e.target === this) hideDeleteModal();
|
|
});
|
|
}
|
|
|
|
// 데이터 로드
|
|
async function loadData() {
|
|
const startDate = document.getElementById('startDate').value;
|
|
const endDate = document.getElementById('endDate').value;
|
|
|
|
if (!startDate || !endDate) {
|
|
alert('시작일과 종료일을 모두 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
if (new Date(startDate) > new Date(endDate)) {
|
|
alert('시작일은 종료일보다 늦을 수 없습니다.');
|
|
return;
|
|
}
|
|
|
|
showLoading(true);
|
|
|
|
try {
|
|
const jsonStr = await machine.Kuntae_GetList(startDate, endDate);
|
|
const data = JSON.parse(jsonStr);
|
|
|
|
if (data) {
|
|
currentData = data;
|
|
renderTable();
|
|
updateStatistics();
|
|
} else {
|
|
currentData = [];
|
|
renderTable();
|
|
}
|
|
} catch (error) {
|
|
console.error('데이터 로드 중 오류 발생:', error);
|
|
alert('데이터를 불러오는 중 오류가 발생했습니다.');
|
|
currentData = [];
|
|
renderTable();
|
|
} finally {
|
|
showLoading(false);
|
|
}
|
|
}
|
|
|
|
// 로딩 상태 표시
|
|
function showLoading(show) {
|
|
const searchBtn = document.getElementById('searchBtn');
|
|
const searchBtnText = document.getElementById('searchBtnText');
|
|
const searchBtnLoading = document.getElementById('searchBtnLoading');
|
|
const loadingRow = document.getElementById('loadingRow');
|
|
|
|
if (show) {
|
|
searchBtn.disabled = true;
|
|
searchBtnText.textContent = '조회 중...';
|
|
searchBtnLoading.classList.remove('hidden');
|
|
loadingRow.classList.remove('hidden');
|
|
} else {
|
|
searchBtn.disabled = false;
|
|
searchBtnText.textContent = '조회';
|
|
searchBtnLoading.classList.add('hidden');
|
|
loadingRow.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
// 테이블 렌더링
|
|
function renderTable() {
|
|
const tbody = document.getElementById('dataTableBody');
|
|
const noDataRow = document.getElementById('noDataRow');
|
|
|
|
// 기존 데이터 행 제거 (로딩, 노데이터 행 제외)
|
|
const existingRows = tbody.querySelectorAll('tr:not(#loadingRow):not(#noDataRow)');
|
|
existingRows.forEach(row => row.remove());
|
|
|
|
if (currentData.length === 0) {
|
|
noDataRow.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
noDataRow.classList.add('hidden');
|
|
|
|
currentData.forEach(item => {
|
|
const row = document.createElement('tr');
|
|
row.className = 'hover:bg-white/10 cursor-pointer transition-colors';
|
|
row.setAttribute('data-id', item.idx);
|
|
|
|
const startDate = item.sdate ? new Date(item.sdate) : null;
|
|
const endDate = item.edate ? new Date(item.edate) : null;
|
|
|
|
row.innerHTML = `
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.cate || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${startDate ? formatDate(startDate) : '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${endDate ? formatDate(endDate) : '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.uid || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.uname || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.term || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.termdr || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.drtime || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.crtime || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white/80 max-w-xs truncate" title="${item.contents || ''}">${item.contents || '-'}</td>
|
|
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.tag || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white"> </td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white"> </td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white"> </td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white"> </td>
|
|
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.extcate || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.wuid || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.wdate || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white/80">
|
|
<div class="flex space-x-2">
|
|
<button class="text-blue-400 hover:text-blue-300 edit-btn transition-colors" data-id="${item.idx}">
|
|
<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 class="text-red-400 hover:text-red-300 delete-btn transition-colors" data-id="${item.idx}">
|
|
<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>
|
|
</div>
|
|
</td>
|
|
|
|
`;
|
|
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
// 행 클릭 이벤트 추가
|
|
tbody.querySelectorAll('tr[data-id]').forEach(row => {
|
|
row.addEventListener('click', function(e) {
|
|
if (!e.target.closest('.edit-btn') && !e.target.closest('.delete-btn')) {
|
|
const id = this.getAttribute('data-id');
|
|
showEditModal(id);
|
|
}
|
|
});
|
|
});
|
|
|
|
// 편집 버튼 이벤트
|
|
tbody.querySelectorAll('.edit-btn').forEach(btn => {
|
|
btn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
const id = this.getAttribute('data-id');
|
|
showEditModal(id);
|
|
});
|
|
});
|
|
|
|
// 삭제 버튼 이벤트
|
|
tbody.querySelectorAll('.delete-btn').forEach(btn => {
|
|
btn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
const id = this.getAttribute('data-id');
|
|
showDeleteModal(id);
|
|
});
|
|
});
|
|
}
|
|
|
|
// 날짜 포맷팅
|
|
function formatDate(date) {
|
|
return date.toLocaleDateString('ko-KR', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit'
|
|
});
|
|
}
|
|
|
|
// 근무시간 계산
|
|
function calculateWorkHours(item) {
|
|
if (!item.intime || !item.outtime) return '-';
|
|
|
|
try {
|
|
const inTime = new Date(`2000-01-01 ${item.intime}`);
|
|
const outTime = new Date(`2000-01-01 ${item.outtime}`);
|
|
|
|
if (outTime < inTime) {
|
|
outTime.setDate(outTime.getDate() + 1);
|
|
}
|
|
|
|
const diffMs = outTime - inTime;
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
|
|
return `${diffHours}시간 ${diffMinutes}분`;
|
|
} catch (error) {
|
|
return '-';
|
|
}
|
|
}
|
|
|
|
// 상태 판단
|
|
function getStatus(item) {
|
|
if (!item.intime) return '결근';
|
|
if (!item.outtime) return '출근';
|
|
|
|
// 지각 판단 (예: 9시 이후 출근)
|
|
const inTime = new Date(`2000-01-01 ${item.intime}`);
|
|
const lateThreshold = new Date(`2000-01-01 09:00:00`);
|
|
|
|
if (inTime > lateThreshold) return '지각';
|
|
return '정상';
|
|
}
|
|
|
|
// 상태별 CSS 클래스
|
|
function getStatusClass(status) {
|
|
switch (status) {
|
|
case '정상': return 'bg-green-100 text-green-800';
|
|
case '지각': return 'bg-yellow-100 text-yellow-800';
|
|
case '결근': return 'bg-red-100 text-red-800';
|
|
case '출근': return 'bg-blue-100 text-blue-800';
|
|
default: return 'bg-gray-100 text-gray-800';
|
|
}
|
|
}
|
|
|
|
// 통계 업데이트
|
|
function updateStatistics() {
|
|
const totalDays = currentData.length;
|
|
const normalDays = currentData.filter(item => getStatus(item) === '정상').length;
|
|
const lateDays = currentData.filter(item => getStatus(item) === '지각').length;
|
|
const absentDays = currentData.filter(item => getStatus(item) === '결근').length;
|
|
|
|
document.getElementById('totalDays').textContent = totalDays;
|
|
document.getElementById('normalDays').textContent = normalDays;
|
|
document.getElementById('lateDays').textContent = lateDays;
|
|
document.getElementById('absentDays').textContent = absentDays;
|
|
}
|
|
|
|
// 근태 추가 모달 표시
|
|
function showAddModal() {
|
|
currentEditId = null;
|
|
document.getElementById('modalTitle').textContent = '근태 추가';
|
|
document.getElementById('editId').value = '';
|
|
document.getElementById('modalDate').value = new Date().toISOString().split('T')[0];
|
|
document.getElementById('modalInTime').value = '';
|
|
document.getElementById('modalOutTime').value = '';
|
|
document.getElementById('modalMemo').value = '';
|
|
document.getElementById('kuntaeModal').classList.remove('hidden');
|
|
}
|
|
|
|
// 근태 편집 모달 표시
|
|
function showEditModal(id) {
|
|
const item = currentData.find(data => (data.id || data.wdate) == id);
|
|
if (!item) {
|
|
alert('데이터를 찾을 수 없습니다.');
|
|
return;
|
|
}
|
|
|
|
currentEditId = id;
|
|
document.getElementById('modalTitle').textContent = '근태 편집';
|
|
document.getElementById('editId').value = id;
|
|
document.getElementById('modalDate').value = item.pdate ? new Date(item.pdate).toISOString().split('T')[0] : '';
|
|
document.getElementById('modalInTime').value = item.intime || '';
|
|
document.getElementById('modalOutTime').value = item.outtime || '';
|
|
document.getElementById('modalMemo').value = item.memo || '';
|
|
document.getElementById('kuntaeModal').classList.remove('hidden');
|
|
}
|
|
|
|
// 근태 모달 숨기기
|
|
function hideKuntaeModal() {
|
|
document.getElementById('kuntaeModal').classList.add('hidden');
|
|
currentEditId = null;
|
|
}
|
|
|
|
// 근태 저장
|
|
async function saveKuntae(e) {
|
|
e.preventDefault();
|
|
|
|
const formData = new FormData(e.target);
|
|
const data = {
|
|
id: formData.get('id'),
|
|
pdate: formData.get('pdate'),
|
|
intime: formData.get('intime'),
|
|
outtime: formData.get('outtime'),
|
|
memo: formData.get('memo')
|
|
};
|
|
|
|
if (!data.pdate) {
|
|
alert('날짜를 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const url = currentEditId ? '/Kuntae/Update' : '/Kuntae/Insert';
|
|
const method = currentEditId ? 'PUT' : 'POST';
|
|
|
|
const response = await axios({
|
|
method: method,
|
|
url: url,
|
|
data: data,
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (response.data && response.data.success) {
|
|
alert(currentEditId ? '근태가 수정되었습니다.' : '근태가 추가되었습니다.');
|
|
hideKuntaeModal();
|
|
loadData(); // 목록 새로고침
|
|
} else {
|
|
alert(response.data?.message || '저장 중 오류가 발생했습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('저장 중 오류 발생:', error);
|
|
alert('저장 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
|
|
// 삭제 모달 표시
|
|
function showDeleteModal(id) {
|
|
const item = currentData.find(data => (data.id || data.wdate) == id);
|
|
if (!item) {
|
|
alert('데이터를 찾을 수 없습니다.');
|
|
return;
|
|
}
|
|
|
|
const date = new Date(item.pdate);
|
|
document.getElementById('deleteConfirmText').textContent =
|
|
`${formatDate(date)} 근태 데이터를 삭제하시겠습니까?`;
|
|
|
|
document.getElementById('confirmDeleteBtn').setAttribute('data-id', id);
|
|
document.getElementById('deleteModal').classList.remove('hidden');
|
|
}
|
|
|
|
// 삭제 모달 숨기기
|
|
function hideDeleteModal() {
|
|
document.getElementById('deleteModal').classList.add('hidden');
|
|
}
|
|
|
|
// 삭제 확인
|
|
async function confirmDelete() {
|
|
const id = document.getElementById('confirmDeleteBtn').getAttribute('data-id');
|
|
|
|
try {
|
|
const jsonStr = await machine.Kuntae_Delete(id);
|
|
const result = JSON.parse(jsonStr);
|
|
|
|
if (result && result.success) {
|
|
alert('근태가 삭제되었습니다.');
|
|
hideDeleteModal();
|
|
loadData(); // 목록 새로고침
|
|
} else {
|
|
alert(result?.message || '삭제 중 오류가 발생했습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('삭제 중 오류 발생:', error);
|
|
alert('삭제 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|