꼭 필요한 정보/우리의 일상에 꼭 필요한 정보

엑셀 없이도 똑똑한 출퇴근 기록, 나만의 웹 앱 만들기!

YTG408 2025. 8. 18.
반응형

많은 분들이 직장이나 개인적으로 출퇴근 시간을 기록할 때 엑셀을 사용하시죠. 하지만 함수를 잘 모르면 복잡하게 느껴지기도 하고, 모바일에서 사용하기 불편할 때도 있습니다. 혹시 엑셀 설치 없이, 그리고 복잡한 수식 없이도 간단하게 출퇴근 기록을 관리하고 싶지 않으신가요?

오늘은 코드를 단 몇 줄만 복사해서 나만의 똑똑한 출퇴근 기록 웹 앱을 만드는 방법을 소개해 드릴게요. 이 앱은 엑셀 시트처럼 연도별로 데이터를 관리하고, 지각이나 초과 근무 여부를 자동으로 계산해주는 아주 유용한 도구랍니다!

왜 엑셀 대신 웹 앱을 사용해야 할까요?

  • 설치 불필요: 별도의 프로그램 설치가 필요 없어요. 인터넷 브라우저(크롬, 엣지, 사파리 등)만 있다면 바로 사용할 수 있습니다.
  • 어디서나 접근 가능: 스마트폰이나 태블릿에서도 똑같이 작동하기 때문에, 출퇴근 시에 바로바로 기록할 수 있어요.
  • 직관적인 인터페이스: 복잡한 메뉴나 툴바 없이, 딱 필요한 입력 창과 버튼만 제공합니다.
  • 자동 계산 기능: 지각이나 초과 근무를 수동으로 계산할 필요 없이, 시간을 입력하는 즉시 결과가 자동으로 표시됩니다.

나만의 웹 앱 기능 살펴보기

이 웹 앱은 총 네 개의 탭으로 구성되어 있습니다. 마치 엑셀 파일의 시트와 같죠!

  1. 2023년, 2024년, 2025년 탭: 각 연도별로 출근 기록을 관리하는 공간이에요.
    • 이름, 날짜, 출근 시간, 퇴근 시간을 입력하면 됩니다.
    • 기본 출근 시간은 오전 9시, 퇴근 시간은 오후 6시로 설정되어 있어요.
    • 출근 시간이 오전 9시를 넘어가면 자동으로 '지각' 표시가 나타납니다.
    • 퇴근 시간이 오후 6시를 넘어가면 자동으로 '초과 근무' 표시가 나타납니다.
  2. 합산 시트: 이 탭은 세 개의 연도 시트(2023, 2024, 2025)에 있는 모든 데이터를 한눈에 요약해 줍니다.
    • 각 직원별 총 지각 횟수총 초과 근무 횟수를 자동으로 계산해줘요.
    • 만약 지각 또는 초과 근무 기록이 하나라도 있으면, 옆에 **✔️(체크 표시)**가 나타나서 '어? 이 사람이 지각한 적이 있네?' 하고 바로 알아볼 수 있습니다.

이 모든 기능은 여러분이 입력하는 데이터에 따라 실시간으로 업데이트되기 때문에, 매우 편리하고 정확하답니다.

웹 앱, 어떻게 만드는 건가요?

"코드는 어려울 것 같은데..." 라고 생각하시는 분들을 위해 간단하게 설명해 드릴게요. 여러분이 사용하게 될 웹 앱은 세 가지 기술로 만들어졌어요.

  • HTML: 웹 페이지의 뼈대를 만드는 역할을 합니다. 제목, 버튼, 표 등이 모두 HTML로 이루어져 있습니다.
  • CSS: 디자인과 스타일을 담당합니다. 글자 색깔, 버튼의 모양, 표의 테두리 등을 예쁘게 꾸며주는 역할을 하죠.
  • JavaScript: 웹 앱에 생명력을 불어넣는 언어입니다. 사용자가 버튼을 클릭하면 새로운 행이 추가되거나, 시간을 계산하여 지각 여부를 표시하는 등의 모든 동적인 기능을 담당합니다.

이 기술들은 모두 웹 브라우저가 이해할 수 있는 언어이기 때문에, 여러분의 컴퓨터에 별도의 프로그램을 설치할 필요가 없는 것이죠!

나만의 웹 앱 사용 방법: 단 3단계!

코드를 복사해서 사용하는 방법은 매우 간단해요. 아래 세 단계만 따라 해 보세요.

1. 코드 복사하기 위에서 제공된 <immersive> 태그 안의 모든 코드를 복사합니다.

2. 파일로 저장하기 메모장(Windows)이나 텍스트 편집기(Mac)를 열고, 복사한 코드를 그대로 붙여넣습니다. 파일을 저장할 때는 파일 이름을 자유롭게 정하되, 뒤에 **.html**이라는 확장자를 꼭 붙여주세요. 예를 들어, attendance.html 또는 출퇴근기록.html처럼요.

3. 파일 열기 저장한 .html 파일을 더블 클릭하면, 자동으로 웹 브라우저가 열리면서 여러분만의 출퇴근 기록 웹 앱이 나타납니다. 이제 데이터를 입력하고 편리하게 사용하시면 됩니다!

html코드

아래는 출석체크를 만든 코드이며 이것을 활용해서 더 꾸미거나 기능을 추가적으로 넣을 실 분들은 넣으면 되고

기존에 만들어 놓은 파일을 사용하시고 싶다면 아래 다운로드 파일을 넣어 드릴게요. 잘 활용해주세요.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>출퇴근 기록 관리</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <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>

다운로드 파일

협회출퇴근체크.html
0.02MB

마무리하며

엑셀이 복잡하게 느껴졌던 분들도 이제는 나만의 웹 앱으로 깔끔하게 출퇴근 기록을 관리할 수 있을 거예요. 

인터넷이 필요 없이 사용가능하고 함계에 누가 지각을 했는지 초과 근무를 했는지 자동으로 시가만 넣으며 표시되니깐 이름과

시간 및 날짜를 입력 후 사용하시면 됩니다. 

반응형

댓글

💲 추천 글