🕒 ‘시간 ~ 시간’ 구간 설정 가능한 웹 타이머 개발기 | 🤖 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를 활용해 간단한 웹페이지 형태로 구현했습니다.
아래 링크에서 실제 동작하는 타이머를 바로 확인하실 수 있어요:
📸 타이머 화면 미리보기
타이머는 크게 두 가지 방식으로 구성되어 있습니다:
지속 시간 방식
5분, 10분, 30분 등 정해진 시간 동안 타이머를 작동시키는 방식
목표 시각 방식
현재 시각 기준으로, 특정 종료 시각을 설정하는 방식
예: “지금이 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
🧾 마무리하며...
결과와 배운점
이번 타이머 프로젝트를 통해,
단순히 시간을 재는 기능을 넘어서 “시간을 구간 단위로 설정한다”는 개념 자체에 더 집중해볼 수 있었습니다.
직접 사용해보니, 정해진 시간보다 "몇 시까지"라는 설정이 훨씬 루틴 관리나 일정 계획에 직관적이라는 걸 확실히 느낄 수 있었고,
이 작은 기능 하나가 실제 삶의 흐름에 더 잘 녹아들 수 있겠다는 확신도 생겼습니다.
에듀테크 스터디에서 더 많은 부분을 배워서 더 실용적인 아이디어들을 기술과 연결해보는 실험들을 꾸준히 이어갈 예정입니다.
봐주셔서 감사합니다! 🙏🙂