많은 분들이 직장이나 개인적으로 출퇴근 시간을 기록할 때 엑셀을 사용하시죠. 하지만 함수를 잘 모르면 복잡하게 느껴지기도 하고, 모바일에서 사용하기 불편할 때도 있습니다. 혹시 엑셀 설치 없이, 그리고 복잡한 수식 없이도 간단하게 출퇴근 기록을 관리하고 싶지 않으신가요?
이 웹 앱은 총 네 개의 탭으로 구성되어 있습니다. 마치 엑셀 파일의 시트와 같죠!
이 모든 기능은 여러분이 입력하는 데이터에 따라 실시간으로 업데이트되기 때문에, 매우 편리하고 정확하답니다.
코드를 복사해서 사용하는 방법은 매우 간단해요. 아래 세 단계만 따라 해 보세요.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>출퇴근 기록 관리</title>
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #f3f4f6;
color: #374151;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.tab-button.active {
background-color: #fff;
border-bottom-color: #10b981;
color: #10b981;
font-weight: 600;
}
</style>
</head>
<body class="p-8">
<div class="max-w-4xl mx-auto bg-white shadow-xl rounded-xl p-6 md:p-8">
<h1 class="text-3xl font-bold mb-6 text-center text-gray-800">출퇴근 기록 관리</h1>
<!-- Tab Navigation -->
<div class="flex border-b border-gray-200 mb-6">
<button id="tab-2023-btn" class="tab-button active flex-1 py-3 px-4 text-center text-sm md:text-base border-b-4 border-transparent rounded-t-lg transition-colors duration-200 ease-in-out">2023년</button>
<button id="tab-2024-btn" class="tab-button flex-1 py-3 px-4 text-center text-sm md:text-base border-b-4 border-transparent rounded-t-lg transition-colors duration-200 ease-in-out">2024년</button>
<button id="tab-2025-btn" class="tab-button flex-1 py-3 px-4 text-center text-sm md:text-base border-b-4 border-transparent rounded-t-lg transition-colors duration-200 ease-in-out">2025년</button>
<button id="tab-summary-btn" class="tab-button flex-1 py-3 px-4 text-center text-sm md:text-base border-b-4 border-transparent rounded-t-lg transition-colors duration-200 ease-in-out">합산 시트</button>
</div>
<!-- 2023년 시트 -->
<div id="tab-2023" class="tab-content active p-4">
<h2 class="text-2xl font-semibold mb-4">2023년 기록</h2>
<div class="overflow-x-auto rounded-lg shadow-md">
<table class="min-w-full bg-white rounded-lg table-auto">
<thead>
<tr class="bg-gray-100 text-gray-600 uppercase text-sm leading-normal">
<th class="py-3 px-4 md:px-6 text-left">이름</th>
<th class="py-3 px-4 md:px-6 text-left">날짜</th>
<th class="py-3 px-4 md:px-6 text-left">출근 시간</th>
<th class="py-3 px-4 md:px-6 text-left">퇴근 시간</th>
<th class="py-3 px-4 md:px-6 text-left">지각</th>
<th class="py-3 px-4 md:px-6 text-left">초과 근무</th>
</tr>
</thead>
<tbody id="attendance-body-2023" class="text-gray-600 text-sm font-light">
</tbody>
</table>
</div>
<button onclick="addRow('2023')" class="mt-4 w-full md:w-auto px-6 py-3 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg shadow-md transition-colors duration-200 ease-in-out">
+ 행 추가
</button>
</div>
<!-- 2024년 시트 -->
<div id="tab-2024" class="tab-content p-4">
<h2 class="text-2xl font-semibold mb-4">2024년 기록</h2>
<div class="overflow-x-auto rounded-lg shadow-md">
<table class="min-w-full bg-white rounded-lg table-auto">
<thead>
<tr class="bg-gray-100 text-gray-600 uppercase text-sm leading-normal">
<th class="py-3 px-4 md:px-6 text-left">이름</th>
<th class="py-3 px-4 md:px-6 text-left">날짜</th>
<th class="py-3 px-4 md:px-6 text-left">출근 시간</th>
<th class="py-3 px-4 md:px-6 text-left">퇴근 시간</th>
<th class="py-3 px-4 md:px-6 text-left">지각</th>
<th class="py-3 px-4 md:px-6 text-left">초과 근무</th>
</tr>
</thead>
<tbody id="attendance-body-2024" class="text-gray-600 text-sm font-light">
</tbody>
</table>
</div>
<button onclick="addRow('2024')" class="mt-4 w-full md:w-auto px-6 py-3 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg shadow-md transition-colors duration-200 ease-in-out">
+ 행 추가
</button>
</div>
<!-- 2025년 시트 -->
<div id="tab-2025" class="tab-content p-4">
<h2 class="text-2xl font-semibold mb-4">2025년 기록</h2>
<div class="overflow-x-auto rounded-lg shadow-md">
<table class="min-w-full bg-white rounded-lg table-auto">
<thead>
<tr class="bg-gray-100 text-gray-600 uppercase text-sm leading-normal">
<th class="py-3 px-4 md:px-6 text-left">이름</th>
<th class="py-3 px-4 md:px-6 text-left">날짜</th>
<th class="py-3 px-4 md:px-6 text-left">출근 시간</th>
<th class="py-3 px-4 md:px-6 text-left">퇴근 시간</th>
<th class="py-3 px-4 md:px-6 text-left">지각</th>
<th class="py-3 px-4 md:px-6 text-left">초과 근무</th>
</tr>
</thead>
<tbody id="attendance-body-2025" class="text-gray-600 text-sm font-light">
</tbody>
</table>
</div>
<button onclick="addRow('2025')" class="mt-4 w-full md:w-auto px-6 py-3 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg shadow-md transition-colors duration-200 ease-in-out">
+ 행 추가
</button>
</div>
<!-- 합산 시트 -->
<div id="tab-summary" class="tab-content p-4">
<h2 class="text-2xl font-semibold mb-4">전체 합산 결과</h2>
<div class="overflow-x-auto rounded-lg shadow-md">
<table class="min-w-full bg-white rounded-lg table-auto">
<thead>
<tr class="bg-gray-100 text-gray-600 uppercase text-sm leading-normal">
<th class="py-3 px-4 md:px-6 text-left">이름</th>
<th class="py-3 px-4 md:px-6 text-left">총 지각 횟수</th>
<th class="py-3 px-4 md:px-6 text-left">지각 여부</th>
<th class="py-3 px-4 md:px-6 text-left">총 초과 근무 횟수</th>
<th class="py-3 px-4 md:px-6 text-left">초과 근무 여부</th>
</tr>
</thead>
<tbody id="summary-body" class="text-gray-600 text-sm font-light">
<tr><td colspan="5" class="py-3 px-4 md:px-6 text-center">각 연도 시트에 데이터를 입력해 주세요.</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
// Data storage for each year
let attendanceData = {
'2023': [],
'2024': [],
'2025': []
};
const standardCheckInTime = "09:00";
const standardCheckOutTime = "18:00";
const years = ['2023', '2024', '2025'];
// Get all DOM elements
const tabButtons = document.querySelectorAll('.tab-button');
const tabContents = document.querySelectorAll('.tab-content');
const attendanceBodies = {
'2023': document.getElementById('attendance-body-2023'),
'2024': document.getElementById('attendance-body-2024'),
'2025': document.getElementById('attendance-body-2025')
};
const summaryBody = document.getElementById('summary-body');
// Function to switch between tabs
function showTab(tabId) {
tabContents.forEach(content => content.classList.remove('active'));
tabButtons.forEach(button => button.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
document.getElementById(tabId + '-btn').classList.add('active');
// If the summary tab is active, update the summary data
if (tabId === 'tab-summary') {
updateSummary();
}
}
// Add event listeners to tab buttons
tabButtons.forEach(button => {
button.addEventListener('click', () => {
const tabId = button.id.replace('-btn', '');
showTab(tabId);
});
});
// Function to add a new row to a specific year's table
function addRow(year) {
const tableBody = attendanceBodies[year];
const newRow = tableBody.insertRow();
// Name input cell
const nameCell = newRow.insertCell(0);
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.placeholder = '이름 입력';
nameInput.className = 'w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-md p-2';
nameInput.addEventListener('input', () => updateRow(newRow, year));
nameCell.appendChild(nameInput);
// Date input cell
const dateCell = newRow.insertCell(1);
const dateInput = document.createElement('input');
dateInput.type = 'date';
dateInput.className = 'w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-md p-2';
dateInput.addEventListener('input', () => updateRow(newRow, year));
dateCell.appendChild(dateInput);
// Check-in time input cell
const checkInCell = newRow.insertCell(2);
const checkInInput = document.createElement('input');
checkInInput.type = 'time';
checkInInput.className = 'w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-md p-2';
checkInInput.addEventListener('input', () => updateRow(newRow, year));
checkInCell.appendChild(checkInInput);
// Check-out time input cell
const checkOutCell = newRow.insertCell(3);
const checkOutInput = document.createElement('input');
checkOutInput.type = 'time';
checkOutInput.className = 'w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-md p-2';
checkOutInput.addEventListener('input', () => updateRow(newRow, year));
checkOutCell.appendChild(checkOutInput);
// Tardy status cell
newRow.insertCell(4).className = 'py-3 px-4 md:px-6';
// Overtime status cell
newRow.insertCell(5).className = 'py-3 px-4 md:px-6';
// Add the new row data to the in-memory data store
attendanceData[year].push({
name: '',
date: '',
checkIn: '',
checkOut: ''
});
// Update the summary whenever a new row is added
updateSummary();
}
// Function to update a row's status based on time inputs
function updateRow(row, year) {
const name = row.cells[0].querySelector('input').value;
const date = row.cells[1].querySelector('input').value;
const checkIn = row.cells[2].querySelector('input').value;
const checkOut = row.cells[3].querySelector('input').value;
// Find the corresponding data object and update it
const rowIndex = Array.from(row.parentNode.children).indexOf(row);
if (attendanceData[year][rowIndex]) {
attendanceData[year][rowIndex] = { name, date, checkIn, checkOut };
}
// Check for tardy
const isTardy = checkIn > standardCheckInTime && checkIn !== '';
row.cells[4].innerHTML = isTardy ? '지각' : '';
// Check for overtime
const isOvertime = checkOut > standardCheckOutTime && checkOut !== '';
row.cells[5].innerHTML = isOvertime ? '초과' : '';
// Update the summary after any change
updateSummary();
}
// Function to update the summary sheet
function updateSummary() {
// Clear the existing summary table body
summaryBody.innerHTML = '';
const summary = {};
// Aggregate data from all years
years.forEach(year => {
attendanceData[year].forEach(record => {
if (record.name.trim() === '') return; // Skip records with no name
const name = record.name;
if (!summary[name]) {
summary[name] = { tardyCount: 0, overtimeCount: 0 };
}
// Check for tardiness
if (record.checkIn > standardCheckInTime && record.checkIn !== '') {
summary[name].tardyCount++;
}
// Check for overtime
if (record.checkOut > standardCheckOutTime && record.checkOut !== '') {
summary[name].overtimeCount++;
}
});
});
// Render the summary table
if (Object.keys(summary).length === 0) {
// If no data, show a message
const emptyRow = summaryBody.insertRow();
const cell = emptyRow.insertCell(0);
cell.colSpan = 5;
cell.className = 'py-3 px-4 md:px-6 text-center text-gray-500';
cell.textContent = '각 연도 시트에 데이터를 입력해 주세요.';
} else {
// Render the aggregated data
for (const name in summary) {
const row = summaryBody.insertRow();
row.className = 'border-b border-gray-200 hover:bg-gray-100';
const nameCell = row.insertCell(0);
nameCell.className = 'py-3 px-4 md:px-6 text-left';
nameCell.textContent = name;
const tardyCountCell = row.insertCell(1);
tardyCountCell.className = 'py-3 px-4 md:px-6 text-center';
tardyCountCell.textContent = summary[name].tardyCount;
const tardyCheckCell = row.insertCell(2);
tardyCheckCell.className = 'py-3 px-4 md:px-6 text-center text-xl text-red-500';
tardyCheckCell.textContent = summary[name].tardyCount > 0 ? '✔️' : '';
const overtimeCountCell = row.insertCell(3);
overtimeCountCell.className = 'py-3 px-4 md:px-6 text-center';
overtimeCountCell.textContent = summary[name].overtimeCount;
const overtimeCheckCell = row.insertCell(4);
overtimeCheckCell.className = 'py-3 px-4 md:px-6 text-center text-xl text-red-500';
overtimeCheckCell.textContent = summary[name].overtimeCount > 0 ? '✔️' : '';
}
}
}
// Initial setup on page load
document.addEventListener('DOMContentLoaded', () => {
showTab('tab-2023');
updateSummary();
});
</script>
</body>
</html>
댓글