‘수업시간 ~ 시간’ 구간 설정 가능한 웹 타이머 개발기

🕒 ‘시간 ~ 시간’ 구간 설정 가능한 웹 타이머 개발기 | 🤖 Claude + 🌐 Google Site 활용
개요:
토론수업이나 쉬는시간에 맞춤형 타이머 개발

🧠 기존 타이머에 ‘시간 구간 설정’ 기능을 더해봤습니다! ⏰

요즘 다양한 학습 타이머나 집중 도구들이 많이 있지만, 대부분은 몇 분, 몇 초 단위로 설정하는 방식이 많습니다. ⏳

이런 타이머들도 충분히 유용하긴 하지만,저는 거기서 한 걸음 더 나아가고 싶었습니다. 🚀

“지금이 오후 2시 10분이면, 딱 3시까지 타이머를 맞추고 싶은데…

왜 시간대 구간을 설정하는 방식은 잘 없을까?” 🤔

기존 타이머의 한계는 바로 이 부분에 있었어요. ⚠️

시간의 길이가 아니라, “시간 구간 자체”를 설정할 수 있으면, 일정 관리나 루틴 설계에 훨씬 직관적일 거라 생각했습니다. 📅✨

그래서 저는 직접, “현재 시각 기준으로 시간 ~ 시간 구간을 설정할 수 있는 타이머 기능” ⏲️을 구현해보기로 했습니다. 💪

✏️ 설계

사용한 도구는 다음과 같습니다:

  • Monica AI (Claude 3.7 Sonnet 기반 인공지능 도구)

    → 타이머의 작동 방식과 화면 구성을 만드는 데 도움을 받았어요.

    (원하는 기능을 말하면, 관련된 코드를 자동으로 만들어주는 AI입니다.)

  • Google Site

    → 구글에서 제공하는 홈페이지 만들기 도구로,

    코딩 없이도 버튼, 글자, 박스 등을 배치해서 웹 화면을 구성할 수 있는 서비스예요.


이번 타이머는 기능을 두 가지 방식으로 나누어 구성했습니다:

  • 📌 지속 시간 탭

    • 기존에 많이 사용하는 방식으로,

      ‘5분, 10분, 15분, 30분, 1시간’ 중 하나를 고르면

      그 시간 동안 타이머가 작동합니다.

    • 화면 아래쪽에는 남은 시간이 보이고,

      ‘시작, 일시정지, 정지, 리셋’ 버튼으로 타이머를 제어할 수 있게 만들었습니다.

  • 🎯 목표 시각 탭

    • 이 방식은 현재 시각을 기준으로, 종료할 시간을 직접 설정하는 방식입니다.

      예: “지금 7시 20분 → 8시까지 타이머 설정”

    • 상단에는 오늘 날짜와 현재 시간이 자동 표시되며,

      아래쪽에는 종료할 시간을 직접 선택할 수 있는 공간이 있고,

      마찬가지로 ‘시작, 일시정지, 정지, 리셋’ 버튼이 배치되어 있습니다.

🌐 타이머 웹페이지 바로가기

타이머는 직접 사용할 수 있도록

Google Site를 활용해 간단한 웹페이지 형태로 구현했습니다.

아래 링크에서 실제 동작하는 타이머를 바로 확인하실 수 있어요:

📸 타이머 화면 미리보기

타이머는 크게 두 가지 방식으로 구성되어 있습니다:

  1. 지속 시간 방식

    • 5분, 10분, 30분 등 정해진 시간 동안 타이머를 작동시키는 방식

  2. 목표 시각 방식

    • 현재 시각 기준으로, 특정 종료 시각을 설정하는 방식

    • 예: “지금이 14:20이면, 15:00까지 타이머 설정”

아래 이미지를 통해 각각의 타이머 UI를 미리 확인해보실 수 있어요 👇

  • 사진들

💻 HTML 코드도 함께 제공합니다

직접 사용해보고 싶으신 분들을 위해,

타이머 UI와 기능을 구현한 HTML 코드도 함께 첨부합니다.

코드는 Monica AI(Claude 3.7 Sonnet 기반 LLM)를 활용하여 생성되었으며,

간단히 Google Site 등 웹 플랫폼에 삽입하면 그대로 구현 가능합니다.

  • 코드 제공

    <!DOCTYPE html>
    <html lang="ko">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>교육용 카운트다운 타이머</title>
        <!-- Google Fonts - Quicksand 추가 -->
        <link href="<https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;600;700&display=swap>" rel="stylesheet">
        <!-- Howler.js 라이브러리 추가 -->
        <script src="<https://cdnjs.cloudflare.com/ajax/libs/howler/2.2.3/howler.min.js>"></script> 
    
      
        <style>
          
    body {
        font-family: 'Quicksand', sans-serif; /* 둥글둥글한 Quicksand 폰트 적용 */
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
        background-color: #FFFDE7; /* 연한 크림색 (Light Cream) */
        color: #333; /* 기본 텍스트 색상 */
        transform: scale(1.25); /* 125% 크기로 확대 */
        transform-origin: center; /* 중앙을 기준으로 확대 (flex 중앙 정렬과 함께 사용) */
    }
    
    .container {
        text-align: center;
        background: #ffffff; /* 밝은 컨테이너 배경 */
        border: 1px solid #e0e0e0;
        border-radius: 20px; /* 둥근 모서리 */
        padding: 40px;
        box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); /* 부드러운 그림자 */
        width: 90%;
        max-width: 600px;
    }
    
    h1 {
        font-size: 2rem;
        color: #4CAF50; /* 밝고 생동감 있는 초록색 */
        margin-bottom: 20px;
        font-weight: 600; /* 강조된 타이틀 */
    }
    
    .timer-display {
        font-size: 4rem;
        font-weight: 700;
        color: #FF6F61; /* 포인트 컬러: 밝은 코랄색 */
        margin: 20px 0;
        transition: transform 0.3s ease, color 0.3s ease;
    }
    
    .timer-display:hover {
        transform: scale(1.1);
        color: #ff8a80; /* 강조 시 밝은 색상 변화 */
    }
    
    .controls, .preset-times {
        margin: 20px 0;
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        gap: 15px;
    }
    
    button {
        padding: 12px 20px;
        font-size: 1rem;
        font-weight: 600;
        border: none;
        border-radius: 30px; /* 둥근 버튼 */
        cursor: pointer;
        transition: background-color 0.3s, transform 0.3s;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* 버튼 그림자 */
        font-family: 'Quicksand', sans-serif; /* 버튼에도 둥근 폰트 적용 */
    }
    
    button:hover {
        transform: scale(1.05);
    }
    
    .preset-btn {
        background-color: #f0f0f0;
        color: #333;
    }
    
    .preset-btn:hover {
        background-color: #e0e0e0;
    }
    
    .control-btn {
        background-color: #4CAF50;
        color: white;
    }
    
    .control-btn:hover {
        background-color: #45a049;
    }
    
    #stop {
        background-color: #FF6F61;
    }
    
    #stop:hover {
        background-color: #ff8a80;
    }
    
    #reset {
        background-color: #2196F3;
    }
    
    #reset:hover {
        background-color: #0b7dda;
    }
    
    .custom-time, .target-time {
        display: flex;
        align-items: center;
        justify-content: center;
        margin: 20px 0;
        flex-wrap: wrap;
        gap: 10px;
    }
    
    input {
        padding: 10px;
        font-size: 1rem;
        border: 1px solid #ccc;
        border-radius: 10px; /* 둥근 입력 필드 */
        width: 70px;
        text-align: center;
        background: #f9f9f9; /* 밝은 입력 필드 배경 */
        color: #333;
        font-family: 'Quicksand', sans-serif; /* 입력 필드에도 둥근 폰트 적용 */
    }
    
    input:focus {
        outline: none;
        border-color: #4CAF50; /* 포커스 시 초록색 강조 */
        box-shadow: 0 0 5px rgba(76, 175, 80, 0.5);
    }
    
    .time-label {
        color: #555;
        margin: 0 5px;
        font-family: 'Quicksand', sans-serif; /* 시간 레이블에도 폰트 적용 */
    }
    
    .alert {
        margin-top: 20px;
        padding: 10px;
        border-radius: 10px;
        background-color: #ffeb3b;
        color: #333;
        font-weight: bold;
        display: none;
        font-family: 'Quicksand', sans-serif; /* 알림에도 폰트 적용 */
    }
    
    .tab-buttons {
        display: flex;
        justify-content: center;
        margin-bottom: 15px;
    }
    
    .tab-btn {
        padding: 10px 20px;
        background-color: #f0f0f0;
        border: none;
        cursor: pointer;
        transition: background-color 0.3s;
        border-radius: 25px;
        margin: 0 5px;
        color: #333;
        font-weight: 600;
        font-family: 'Quicksand', sans-serif; /* 탭 버튼에도 폰트 적용 */
    }
    
    .tab-btn.active {
        background-color: #4CAF50;
        color: #ffffff;
    }
    
    .tab-content {
        display: none;
    }
    
    .tab-content.active {
        display: block;
    }
    
    .current-time {
        font-size: 1.2rem;
        margin: 10px 0;
        color: #2196F3; /* 밝은 파란색 */
        font-weight: bold;
        font-family: 'Quicksand', sans-serif; /* 현재 시간에도 폰트 적용 */
    }
    
    @media (max-width: 600px) {
        .timer-display {
            font-size: 3rem;
        }
        
        .controls {
            flex-direction: column;
        }
    }
    
        </style>
    </head>
    <body>
        <div class="container">
            <h1>⭐CountDown Timer⭐</h1>
            
            <div class="tab-container">
                <div class="tab-buttons">
                    <button class="tab-btn active" data-tab="duration">지속 시간</button>
                    <button class="tab-btn" data-tab="target">목표 시각</button>
                </div>
                
                <div id="duration-tab" class="tab-content active">
                    <div class="preset-times">
                        <button class="preset-btn" data-time="300">5분</button>
                        <button class="preset-btn" data-time="600">10분</button>
                        <button class="preset-btn" data-time="900">15분</button>
                        <button class="preset-btn" data-time="1800">30분</button>
                        <button class="preset-btn" data-time="3600">1시간</button>
                    </div>
                    
                    <div class="custom-time">
                        <input type="number" id="hours" min="0" max="23" value="0">
                        <span class="time-label">시간</span>
                        <input type="number" id="minutes" min="0" max="59" value="5">
                        <span class="time-label"></span>
                        <input type="number" id="seconds" min="0" max="59" value="0">
                        <span class="time-label"></span>
                        <button id="set-custom" class="control-btn">설정</button>
                    </div>
                </div>
                
                <div id="target-tab" class="tab-content">
                    <div class="current-time" id="current-date">날짜 로딩중...</div>
                    <div class="current-time" id="current-time">현재 시간: 00:00:00</div>
                    <div class="target-time">
                        <input type="number" id="target-hours" min="0" max="23" value="0">
                        <span class="time-label"></span>
                        <input type="number" id="target-minutes" min="0" max="59" value="0">
                        <span class="time-label"></span>
                        <input type="number" id="target-seconds" min="0" max="59" value="0">
                        <span class="time-label"></span>
                        <button id="set-target" class="control-btn">설정</button>
                    </div>
                </div>
            </div>
            
            <div class="timer-display" id="timer">05:00</div>
            
            <div class="controls">
                <button id="start" class="control-btn">시작</button>
                <button id="pause" class="control-btn">일시정지</button>
                <button id="stop" class="control-btn">중지</button>
                <button id="reset" class="control-btn">리셋</button>
            </div>
            
            <div class="alert" id="alert">시간이 종료되었습니다!</div>
        </div>
    
        <script>
            document.addEventListener('DOMContentLoaded', function() {
                // 요소 가져오기
                const timerDisplay = document.getElementById('timer');
                const startBtn = document.getElementById('start');
                const pauseBtn = document.getElementById('pause');
                const stopBtn = document.getElementById('stop');
                const resetBtn = document.getElementById('reset');
                const setCustomBtn = document.getElementById('set-custom');
                const setTargetBtn = document.getElementById('set-target');
                const hoursInput = document.getElementById('hours');
                const minutesInput = document.getElementById('minutes');
                const secondsInput = document.getElementById('seconds');
                const targetHoursInput = document.getElementById('target-hours');
                const targetMinutesInput = document.getElementById('target-minutes');
                const targetSecondsInput = document.getElementById('target-seconds');
                const presetButtons = document.querySelectorAll('.preset-btn');
                const alertBox = document.getElementById('alert');
                const tabButtons = document.querySelectorAll('.tab-btn');
                const tabContents = document.querySelectorAll('.tab-content');
                const currentTimeDisplay = document.getElementById('current-time');
                const currentDateDisplay = document.getElementById('current-date');
                
                let countdown;
                let totalSeconds = 300; // 기본값 5분
                let isRunning = false;
                let originalTime = totalSeconds;
                let isTargetMode = false;
                let targetTime = null;
                
                // 알람 소리를 위한 전역 변수
                let alarmSound = null;
                
                // 알람 소리 재생 함수
                function playAlarmSound() {
                    // 이미 재생 중인 알람이 있으면 중지
                    stopAlarmSound();
                    
                    // 새 알람 소리 생성 및 재생
                    alarmSound = new Howl({
                        src: ['<https://actions.google.com/sounds/v1/alarms/medium_bell_ringing_near.ogg>'],
                        loop: true,
                        volume: 0.8,
                        html5: true
                    });
                    
                    alarmSound.play();
                }
                
                // 알람 소리 중지 함수
                function stopAlarmSound() {
                    if (alarmSound) {
                        alarmSound.stop();
                        alarmSound.unload(); // 리소스 완전 해제(stop() 함수만으로는 부족해서)
                        alarmSound = null;
                    }
                  
                    // 전역 Howler 객체의 모든 사운드 중지 시도
                    if (typeof Howler !== 'undefined') {
                        Howler.unload();
                    }
                }
                
                // 현재 시간 업데이트 함수
                function updateCurrentTime() {
                    const now = new Date();
                    
                    // 요일 배열 (한글, 영어)
                    const weekdaysKo = ['일', '월', '화', '수', '목', '금', '토'];
                    const weekdaysEn = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
                    
                    // 날짜 및 요일 포맷팅
                    const year = now.getFullYear();
                    const month = (now.getMonth() + 1).toString().padStart(2, '0');
                    const date = now.getDate().toString().padStart(2, '0');
                    const weekdayKo = weekdaysKo[now.getDay()];
                    const weekdayEn = weekdaysEn[now.getDay()];
                    
                    // 날짜와 요일 표시
                    currentDateDisplay.textContent = `${year}-${month}-${date} ${weekdayKo}요일 (${weekdayEn})`;
                    
                    // 시간 포맷팅 (기존 코드)
                    const hours = now.getHours().toString().padStart(2, '0');
                    const minutes = now.getMinutes().toString().padStart(2, '0');
                    const seconds = now.getSeconds().toString().padStart(2, '0');
                    currentTimeDisplay.textContent = `현재 시간: ${hours}:${minutes}:${seconds}`;
                    
                    // 목표 시간 모드에서 실행 중이면 남은 시간 업데이트 (기존 코드)
                    if (isRunning && isTargetMode && targetTime) {
                        const diff = Math.max(0, Math.floor((targetTime - now) / 1000));
                        if (diff !== totalSeconds) {
                            totalSeconds = diff;
                            updateTimerDisplay();
                            
                            if (totalSeconds <= 0) {
                                clearInterval(countdown);
                                isRunning = false;
                                alertBox.style.display = 'block';
                                playAlarmSound();
                            }
                        }
                    }
                }
                
                // 1초마다 현재 시간 업데이트
                setInterval(updateCurrentTime, 1000);
                updateCurrentTime(); // 초기 로드 시 현재 시간 표시
                
                // 목표 시간 설정 함수
                function setTargetTime() {
                    // 알람 소리 중지 (설정 변경 시)
                    stopAlarmSound();
                    
                    const now = new Date();
                    const targetHours = parseInt(targetHoursInput.value) || 0;
                    const targetMinutes = parseInt(targetMinutesInput.value) || 0;
                    const targetSeconds = parseInt(targetSecondsInput.value) || 0;
                    
                    // 목표 시간 설정
                    targetTime = new Date();
                    targetTime.setHours(targetHours);
                    targetTime.setMinutes(targetMinutes);
                    targetTime.setSeconds(targetSeconds);
                    
                    // 목표 시간이 현재 시간보다 이전이면 다음 날로 설정
                    if (targetTime < now) {
                        targetTime.setDate(targetTime.getDate() + 1);
                    }
                    
                    // 남은 시간 계산 (초 단위)
                    totalSeconds = Math.floor((targetTime - now) / 1000);
                    originalTime = totalSeconds;
                    
                    if (totalSeconds > 0) {
                        updateTimerDisplay();
                        alertBox.style.display = 'none';
                        isTargetMode = true;
                    }
                }
                
                // 탭 전환 함수
                tabButtons.forEach(button => {
                    button.addEventListener('click', function() {
                        // 알람 소리 중지 (탭 전환 시)
                        stopAlarmSound();
                        
                        // 활성 탭 변경
                        tabButtons.forEach(btn => btn.classList.remove('active'));
                        this.classList.add('active');
                        
                        // 탭 콘텐츠 변경
                        const tabId = this.getAttribute('data-tab');
                        tabContents.forEach(content => content.classList.remove('active'));
                        document.getElementById(`${tabId}-tab`).classList.add('active');
                        
                        // 목표 시간 모드 설정
                        isTargetMode = (tabId === 'target');
                        
                        // 타이머 초기화
                        clearInterval(countdown);
                        isRunning = false;
                        
                        if (isTargetMode) {
                            // 목표 시간 모드로 전환 시 현재 시간 기준으로 입력 필드 초기화
                            const now = new Date();
                            targetHoursInput.value = now.getHours();
                            targetMinutesInput.value = (now.getMinutes() + 5) % 60; // 기본값: 현재 시간 + 5분
                            targetSecondsInput.value = now.getSeconds();
                            
                            // 분이 넘어가면 시간 조정
                            if (now.getMinutes() > 55) {
                                targetHoursInput.value = (now.getHours() + 1) % 24;
                            }
                        } else {
                            // 지속 시간 모드로 전환 시
                            totalSeconds = 300; // 기본값 5분으로 복원
                            originalTime = totalSeconds;
                            updateTimerDisplay();
                        }
                        
                        // 알림 메시지 숨기기
                        alertBox.style.display = 'none';
                    });
                });
                
                // 타이머 표시 업데이트 함수
                function updateTimerDisplay() {
                    const hours = Math.floor(totalSeconds / 3600);
                    const minutes = Math.floor((totalSeconds % 3600) / 60);
                    const seconds = totalSeconds % 60;
                    
                    const displayHours = hours > 0 ? `${hours.toString().padStart(2, '0')}:` : '';
                    const displayMinutes = `${minutes.toString().padStart(2, '0')}`;
                    const displaySeconds = `${seconds.toString().padStart(2, '0')}`;
                    
                    timerDisplay.textContent = `${displayHours}${displayMinutes}:${displaySeconds}`;
                    
                    // 시간이 다 되면 알림 표시
                    if (totalSeconds === 0) {
                        clearInterval(countdown);
                        isRunning = false;
                        alertBox.style.display = 'block';
                        playAlarmSound();
                    }
                }
                
                // 타이머 시작 함수
                function startTimer() {
                    // 알람 소리 중지
                    stopAlarmSound();
                    
                    if (!isRunning && totalSeconds > 0) {
                        isRunning = true;
                        alertBox.style.display = 'none';
                        
                        if (!isTargetMode) {
                            countdown = setInterval(function() {
                                totalSeconds--;
                                updateTimerDisplay();
                                
                                if (totalSeconds <= 0) {
                                    clearInterval(countdown);
                                    isRunning = false;
                                }
                            }, 1000);
                        }
                        // 목표 시간 모드는 updateCurrentTime에서 처리됨
                    }
                }
                
                // 타이머 일시정지 함수
                function pauseTimer() {
                    // 알람 소리 중지
                    stopAlarmSound();
                    
                    if (isRunning) {
                        clearInterval(countdown);
                        isRunning = false;
                    }
                }
                
                // 타이머 중지 함수
                function stopTimer() {
                    // 알람 소리 중지
                    stopAlarmSound();
                    
                    clearInterval(countdown);
                    isRunning = false;
                    totalSeconds = 0;
                    updateTimerDisplay();
                    alertBox.style.display = 'none';
                  
                    // 100ms(0.1초) 후 한 번 더 알람 중지 시도 (비동기 문제 해결)
                    setTimeout(() => {
                        stopAlarmSound();
                    }, 10);
                }
                
                // 타이머 리셋 함수
                function resetTimer() {
                    // 알람 소리 중지
                    stopAlarmSound();
                    
                    clearInterval(countdown);
                    isRunning = false;
                    totalSeconds = originalTime;
                    updateTimerDisplay();
                    alertBox.style.display = 'none';
                }
                
                // 사용자 정의 시간 설정 함수
                function setCustomTime() {
                    // 알람 소리 중지 (설정 변경 시)
                    stopAlarmSound();
                    
                    const hours = parseInt(hoursInput.value) || 0;
                    const minutes = parseInt(minutesInput.value) || 0;
                    const seconds = parseInt(secondsInput.value) || 0;
                    
                    totalSeconds = hours * 3600 + minutes * 60 + seconds;
                    originalTime = totalSeconds;
                    isTargetMode = false;
                    
                    if (totalSeconds > 0) {
                        updateTimerDisplay();
                        alertBox.style.display = 'none';
                    }
                }
                
                // 이벤트 리스너 등록
                startBtn.addEventListener('click', startTimer);
                pauseBtn.addEventListener('click', pauseTimer);
                stopBtn.addEventListener('click', stopTimer);
                resetBtn.addEventListener('click', resetTimer);
                setCustomBtn.addEventListener('click', setCustomTime);
                setTargetBtn.addEventListener('click', setTargetTime);
                
                // 프리셋 버튼 이벤트 리스너
                presetButtons.forEach(button => {
                    button.addEventListener('click', function() {
                        // 알람 소리 중지 (프리셋 변경 시)
                        stopAlarmSound();
                        
                        totalSeconds = parseInt(this.getAttribute('data-time'));
                        originalTime = totalSeconds;
                        isTargetMode = false;
                        updateTimerDisplay();
                        alertBox.style.display = 'none';
                    });
                });
                
                // 초기 타이머 표시 업데이트
                updateTimerDisplay();
            });
        </script>
    </body>
    </html>
    
    

🎥 시연 영상 안내

해당 타이머의 실제 작동 모습을 영상으로 확인하실 수 있습니다.

사용자 인터페이스(UI), 시간 구간 설정 방식, 전반적인 동작 흐름까지 모두 담겨 있습니다.

0 https://www.youtube.com/watch?v=Wo6EnWhfRI0

🧾 마무리하며...

결과와 배운점

이번 타이머 프로젝트를 통해,

단순히 시간을 재는 기능을 넘어서 “시간을 구간 단위로 설정한다”는 개념 자체에 더 집중해볼 수 있었습니다.

직접 사용해보니, 정해진 시간보다 "몇 시까지"라는 설정이 훨씬 루틴 관리나 일정 계획에 직관적이라는 걸 확실히 느낄 수 있었고,

이 작은 기능 하나가 실제 삶의 흐름에 더 잘 녹아들 수 있겠다는 확신도 생겼습니다.

에듀테크 스터디에서 더 많은 부분을 배워서 더 실용적인 아이디어들을 기술과 연결해보는 실험들을 꾸준히 이어갈 예정입니다.

봐주셔서 감사합니다! 🙏🙂

4
2개의 답글

👉 이 게시글도 읽어보세요