너는 학교 현장을 잘 아는 프론트엔드 개발자야. 초등학교 교실에서 아침 조회 시간이나 쉬는 시간에 띄워둘 '학급 대시보드' 웹페이지(HTML/CSS)를 만들려고 해.
화면은 가로로 넓게 쓰고, 전체적인 배경은 깔끔한 연회색으로 해줘.
맨 위에는 현재 날짜와 시간이 나오는 작은 바를 하나 만들어 줘.
그 아래로 총 6개의 네모난 카드를 바둑판(Grid) 모양으로 배치해 줘. 카드의 배경색은 차분한 파란색(청록색 계열)으로 하고, 글씨는 흰색으로 해줘. 모서리는 둥글게 해줘.
카드의 구성은 다음과 같아:
첫 번째 줄: '칭찬 통장' (1칸 차지), '오늘 할일 안내판' (2칸 차지), '타이머' (1칸 차지)
두 번째 줄: '발표 뽑기' (1칸 차지), '숙제 알림장' (1칸 차지), '오늘의 행사' (1칸 차지), '오늘의 식단' (1칸 차지)
지금은 기능이 작동하지 않아도 되니까, 각 카드 안에 디자인이 예뻐 보이도록 가짜 데이터(예: 오늘의 식단에는 가짜 메뉴들, 칭찬 통장에는 1200점 등)와 어울리는 이모지를 넣어줘.
HTML 단일 파일로 뼈대를 짜줘. CSS는 Tailwind CSS를 사용해.
방금 만들어준 HTML 코드에서 '타이머' 부분을 실제로 작동하게 만들어줘.
타이머 영역 안에는 시간을 보여주는 '00:00' 글씨와 그 아래에 버튼들이 있어야 해.
버튼은 총 5개로 만들어줘: [시작], [정지], [+1분], [+10초], [리셋]
JavaScript를 추가해서 버튼을 누르면 시간이 올라가고, 시작을 누르면 1초씩 줄어들게 해줘.
시간이 00:00이 되면 브라우저 내장 기능(Web Audio API)을 사용해서 '삐- 삐- 삐-' 하는 전자 알람 소리가 나게 해줘. (학교 방화벽 때문에 외부 mp3 링크는 작동하지 않아)
추가로, 타이머 영역 우측 상단에 십자 화살표(확대) 모양의 SVG 아이콘을 하나 넣어줘. 이 아이콘을 누르면 화면 전체를 덮는 거대한 팝업창(모달)이 뜨고, 그 안에서도 커다란 글씨로 타이머를 볼 수 있고 똑같은 버튼들로 조작할 수 있게 해줘.
전체 코드를 다시 주지 말고, 추가되거나 수정되어야 하는 부분(수정된 타이머 HTML 영역과 새로 추가할 JavaScript 코드)만 알려줘.
<!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>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Noto Sans KR', 'sans-serif'],
},
colors: {
'dashboard-blue': '#1a6582', // 이미지와 유사한 청록색 테마
'dashboard-dark': '#134e66'
}
}
}
}
</script>
<style>
/* 스크롤바 숨기기 등 부가적인 스타일 */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
.card-hover:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
</style>
</head>
<body class="bg-gray-50 text-gray-800 p-4 md:p-8 min-h-screen">
<div class="max-w-7xl mx-auto mb-6">
<div class="inline-block bg-dashboard-blue text-white px-6 py-2 shadow-md">
<span id="realtime-clock" class="text-xl font-bold tracking-wider">0000. 00. 00. 00:00</span>
</div>
</div>
<div class="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
<!-- 첫 번째 줄 -->
<!-- 칭찬 통장 (1칸) -->
<div class="bg-dashboard-blue text-white p-6 shadow-lg flex flex-col justify-between card-hover min-h-[250px]">
<h2 class="text-2xl font-bold mb-4 text-center border-b border-white/30 pb-2">칭찬 통장</h2>
<div class="flex-grow flex flex-col items-center justify-center">
<div class="text-5xl mb-2">🌟</div>
<div class="text-4xl font-bold">1,250 <span class="text-lg font-normal">P</span></div>
<p class="text-sm mt-4 text-blue-200">목표 점수: 2,000 P</p>
<div class="w-full bg-dashboard-dark rounded-full h-2.5 mt-2">
<div class="bg-yellow-400 h-2.5 rounded-full" style="width: 62.5%"></div>
</div>
</div>
</div>
<!-- 오늘 할일 안내판 (2칸) -->
<div class="bg-dashboard-blue text-white p-6 shadow-lg lg:col-span-2 flex flex-col card-hover min-h-[250px]">
<h2 class="text-2xl font-bold mb-4 text-center border-b border-white/30 pb-2">오늘 할일 안내판</h2>
<div class="flex-grow grid grid-cols-2 gap-4 mt-2">
<div class="bg-dashboard-dark p-4 rounded-lg shadow-inner">
<h3 class="font-bold text-yellow-300 mb-2">오늘의 시간표</h3>
<ul class="space-y-2 text-sm">
<li><span class="font-bold mr-2">1교시</span> 국어 (5단원)</li>
<li><span class="font-bold mr-2">2교시</span> 수학 (분수)</li>
<li><span class="font-bold mr-2">3교시</span> 체육 (강당)</li>
<li><span class="font-bold mr-2">4교시</span> 과학 (실험실)</li>
<li><span class="font-bold mr-2">5교시</span> 창체 (자율)</li>
</ul>
</div>
<div class="bg-dashboard-dark p-4 rounded-lg shadow-inner flex flex-col">
<h3 class="font-bold text-yellow-300 mb-2">학급 공지사항</h3>
<div class="flex-grow">
<ul class="list-disc list-inside space-y-1 text-sm">
<li>복도에서 뛰지 않기!</li>
<li>내일은 현장체험학습 가는 날입니다.</li>
<li>체육복 꼭 챙겨오세요.</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 타이머 (1칸) -->
<div class="bg-dashboard-blue text-white p-6 shadow-lg flex flex-col justify-between card-hover min-h-[250px] relative">
<!-- 확대 버튼 추가 -->
<button onclick="openTimerModal()" class="absolute top-4 right-4 text-white/50 hover:text-white transition-colors" title="전체화면 확대">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</svg>
</button>
<h2 class="text-2xl font-bold mb-4 text-center border-b border-white/30 pb-2">타이머</h2>
<div class="flex-grow flex flex-col items-center justify-center">
<div id="display-time" class="text-6xl font-bold tracking-widest font-mono mb-4">00:00</div>
<div class="grid grid-cols-2 gap-2 w-full max-w-[200px]">
<button onclick="startTimer()" class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-2 rounded shadow text-sm transition-colors">시작</button>
<button onclick="stopTimer()" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-2 rounded shadow text-sm transition-colors">정지</button>
<button onclick="addOneMinute()" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-2 rounded shadow text-sm transition-colors">+1분</button>
<button onclick="addTenSeconds()" class="bg-indigo-500 hover:bg-indigo-600 text-white font-bold py-2 px-2 rounded shadow text-sm transition-colors">+10초</button>
<button onclick="resetTimer()" class="col-span-2 bg-gray-500 hover:bg-gray-600 text-white font-bold py-2 px-2 rounded shadow text-sm transition-colors">리셋</button>
</div>
</div>
</div>
<!-- 두 번째 줄 -->
<!-- 발표 뽑기 (1칸) -->
<div class="bg-dashboard-blue text-white p-6 shadow-lg flex flex-col justify-between card-hover min-h-[250px]">
<h2 class="text-2xl font-bold mb-4 text-center border-b border-white/30 pb-2">발표 뽑기</h2>
<div class="flex-grow flex flex-col items-center justify-center space-y-4">
<div class="text-6xl text-yellow-300">🙋♂️</div>
<div class="text-2xl font-bold bg-dashboard-dark px-6 py-2 rounded-lg w-full text-center">김*수</div>
<button class="w-full bg-yellow-500 hover:bg-yellow-600 text-dashboard-dark font-bold py-3 px-4 rounded shadow-md transition-colors">
무작위 뽑기
</button>
</div>
</div>
<!-- 숙제 (1칸) -->
<div class="bg-dashboard-blue text-white p-6 shadow-lg flex flex-col card-hover min-h-[250px]">
<h2 class="text-2xl font-bold mb-4 text-center border-b border-white/30 pb-2">숙제 (알림장)</h2>
<div class="flex-grow bg-dashboard-dark p-4 rounded-lg shadow-inner">
<ul class="list-decimal list-inside space-y-3 text-sm">
<li>수학 익힘책 24~25쪽 풀고 채점하기</li>
<li>독서록 1편 쓰기 (자유 주제)</li>
<li>가정통신문(방과후학교 신청) 부모님께 전달하기</li>
</ul>
</div>
</div>
<!-- 오늘의 행사 (1칸) -->
<div class="bg-dashboard-blue text-white p-6 shadow-lg flex flex-col justify-between card-hover min-h-[250px]">
<h2 class="text-2xl font-bold mb-4 text-center border-b border-white/30 pb-2">오늘의 행사</h2>
<div class="flex-grow flex flex-col items-center justify-center">
<div class="text-5xl mb-4">🎪</div>
<div class="text-center">
<p class="font-bold text-xl text-yellow-300 mb-2">교내 과학의 달 행사</p>
<p class="text-sm">장소: 체육관</p>
<p class="text-sm">시간: 3~4교시</p>
</div>
</div>
</div>
<!-- 오늘의 식단 (1칸) -->
<div class="bg-dashboard-blue text-white p-6 shadow-lg flex flex-col card-hover min-h-[250px]">
<h2 class="text-2xl font-bold mb-4 text-center border-b border-white/30 pb-2">오늘의 식단</h2>
<div class="flex-grow bg-dashboard-dark p-4 rounded-lg shadow-inner flex flex-col items-center justify-center text-center">
<div class="text-4xl mb-2">🍱</div>
<ul class="space-y-1 font-medium">
<li>찰보리밥</li>
<li>쇠고기미역국</li>
<li>돼지고기 두루치기</li>
<li>시금치나물</li>
<li>배추김치</li>
<li class="text-yellow-300 pt-2 text-sm border-t border-white/20 mt-2">방울토마토</li>
</ul>
</div>
</div>
</div>
<!-- 타이머 전체화면 모달 (기본 숨김 상태) -->
<div id="timer-modal" class="fixed inset-0 bg-dashboard-dark/95 hidden flex-col items-center justify-center z-50 backdrop-blur-sm">
<!-- 닫기(X) 버튼 -->
<button onclick="closeTimerModal()" class="absolute top-8 right-8 text-white/70 hover:text-white text-5xl transition-colors">×</button>
<!-- 거대한 시간 표시 -->
<div id="modal-display-time" class="text-[12rem] md:text-[18rem] font-bold tracking-widest font-mono mb-12 text-white drop-shadow-2xl">00:00</div>
<!-- 큼직한 조작 버튼들 -->
<div class="grid grid-cols-3 md:grid-cols-5 gap-4 w-full max-w-5xl px-8">
<button onclick="startTimer()" class="bg-green-500 hover:bg-green-600 text-white font-bold py-6 px-4 rounded-2xl shadow-lg text-2xl transition-colors">시작</button>
<button onclick="stopTimer()" class="bg-red-500 hover:bg-red-600 text-white font-bold py-6 px-4 rounded-2xl shadow-lg text-2xl transition-colors">정지</button>
<button onclick="addOneMinute()" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-6 px-4 rounded-2xl shadow-lg text-2xl transition-colors">+1분</button>
<button onclick="addTenSeconds()" class="bg-indigo-500 hover:bg-indigo-600 text-white font-bold py-6 px-4 rounded-2xl shadow-lg text-2xl transition-colors">+10초</button>
<button onclick="resetTimer()" class="col-span-3 md:col-span-1 bg-gray-500 hover:bg-gray-600 text-white font-bold py-6 px-4 rounded-2xl shadow-lg text-2xl transition-colors">리셋</button>
</div>
</div>
<script>
// 실시간 시계 업데이트 기능
function updateClock() {
const now = new Date();
const year = now.getFullYear();
// 월과 일은 2자리 숫자로 맞춤 (예: 07, 03)
const month = String(now.getMonth() + 1).padStart(2, '0');
const date = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const timeString = `${year}. ${month}. ${date}. ${hours}:${minutes}`;
document.getElementById('realtime-clock').textContent = timeString;
}
// 1초마다 시계 업데이트
setInterval(updateClock, 1000);
// 페이지 로드 시 즉시 시계 표시
updateClock();
// --------------------------------------------------
// 타이머 기능 로직 추가
// --------------------------------------------------
let timeLeft = 0; // 남은 시간 (초 단위)
let timerInterval = null; // setInterval 제어용 변수
const displayTimeElement = document.getElementById('display-time');
const modalDisplayTimeElement = document.getElementById('modal-display-time'); // 모달 시간 표시 요소 추가
// 브라우저 내장 오디오(Web Audio API)를 활용하여 외부 링크 차단 문제 해결
let audioCtx;
function initAudio() {
// 브라우저 호환성을 위해 AudioContext 초기화
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
// 브라우저 정책상 정지된 오디오 컨텍스트를 사용자 클릭 시점에 깨워줌
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function playBeepAlarm() {
initAudio();
// 삐- 삐- 삐- 알람 소리 3번 울림
for (let i = 0; i < 3; i++) {
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.type = 'square'; // 알람 소리에 적합한 파형 (전자음 느낌)
oscillator.frequency.setValueAtTime(800, audioCtx.currentTime + i * 0.5); // 주파수(음높이)
// 볼륨 설정 (점점 줄어드는 효과)
gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime + i * 0.5);
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + i * 0.5 + 0.3);
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.start(audioCtx.currentTime + i * 0.5);
oscillator.stop(audioCtx.currentTime + i * 0.5 + 0.3);
}
}
// 화면에 시간을 00:00 형식으로 그려주는 함수
function updateTimerDisplay() {
const minutes = String(Math.floor(timeLeft / 60)).padStart(2, '0');
const seconds = String(timeLeft % 60).padStart(2, '0');
const timeString = `${minutes}:${seconds}`;
displayTimeElement.textContent = timeString; // 대시보드 화면 업데이트
// 모달 화면이 존재하면 동시에 업데이트
if (modalDisplayTimeElement) {
modalDisplayTimeElement.textContent = timeString;
}
}
// 팝업(모달) 열기/닫기 함수 추가
function openTimerModal() {
document.getElementById('timer-modal').classList.remove('hidden');
document.getElementById('timer-modal').classList.add('flex');
}
function closeTimerModal() {
document.getElementById('timer-modal').classList.add('hidden');
document.getElementById('timer-modal').classList.remove('flex');
}
// 시작 버튼 클릭 시
function startTimer() {
initAudio(); // 버튼 클릭 시 오디오 권한 활성화 (브라우저 정책 우회 필수)
if (timerInterval) clearInterval(timerInterval); // 이미 실행 중이면 중복 실행 방지
if (timeLeft <= 0) return; // 0초면 시작하지 않음
timerInterval = setInterval(() => {
timeLeft--;
updateTimerDisplay();
// 타이머가 0이 되었을 때
if (timeLeft <= 0) {
clearInterval(timerInterval);
timerInterval = null;
playBeepAlarm(); // 내장 비프음 알람 발생
}
}, 1000);
}
// 정지 버튼 클릭 시
function stopTimer() {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
}
// +1분 버튼 클릭 시
function addOneMinute() {
timeLeft += 60; // 60초 추가
updateTimerDisplay();
}
// +10초 버튼 클릭 시
function addTenSeconds() {
timeLeft += 10; // 10초 추가
updateTimerDisplay();
}
// 리셋 버튼 클릭 시
function resetTimer() {
stopTimer(); // 진행 중인 타이머 멈춤
timeLeft = 0; // 0초로 초기화
updateTimerDisplay();
}
// 페이지 로드 시 타이머 00:00 표시
updateTimerDisplay();
</script>
</body>
</html>
// 스프레드시트의 '학생명부' 탭에서 학생 이름을 가져오는 API
function doGet(e) {
// 선생님의 스프레드시트 ID (URL 중간에 있는 긴 문자열)
const sheetId = '1l1L0BQCLIs7uoDkipRYKcfEuooXwxFJHMUZEmARYFuc';
const sheet = SpreadsheetApp.openById(sheetId).getSheetByName('학생명부');
// A열(이름) 데이터 가져오기 (1행이 '이름' 등 제목이라고 가정하고 2행부터 마지막 행까지)
// 만약 이름이 B열에 있다면 getRange(2, 2, ...) 로 수정하면 됩니다.
const data = sheet.getRange(2, 2, sheet.getLastRow() - 1, 1).getValues();
// 빈 칸을 제외하고 이름만 있는 깔끔한 리스트(배열)로 만들기
const students = data.map(row => row[0]).filter(name => name !== "");
// 대시보드가 읽을 수 있게 JSON 형태로 포장해서 배달!
return ContentService.createTextOutput(JSON.stringify(students))
.setMimeType(ContentService.MimeType.JSON);
}
기존 대시보드 코드에서 '발표 뽑기' 구역을 다음 조건에 맞게 완전히 업그레이드해 줘.
1. 화면 구성 (UI):
기존 이모지(🙋♂️)를 지우고, 그 자리에 뽑을 인원수를 조절하는 [-] 버튼, 1명 뽑기 텍스트, [+] 버튼을 가로로 나란히 배치해 줘.
그 아래에는 학생 이름이 표시될 넉넉한 공간을 만들어 줘. 여러 명이 뽑힐 경우 이름들이 예쁜 노란색 캡슐(Chip) 모양으로 줄바꿈 되며 나열되도록 해줘.
맨 아래에는 [무작위 뽑기] 버튼을 배치해 줘.
2. 데이터 연동 (GAS 연동 로직):
JavaScript 최상단에 const GAS_WEB_APP_URL = "여기에_자신의_주소_입력"; 변수를 만들어 줘.
fetch 함수를 써서 저 주소에서 학생 명단 배열을 가져오게 해줘. (구글 서버 특성상 fetch 옵션에 { redirect: 'follow' }를 반드시 넣어야 해!)
만약 주소가 비어있거나 불러오는 데 실패하면, 테스트용 가짜 명단(김철수, 이영희 등 8명)을 기본값으로 사용하게 해줘.
3. 무작위 뽑기 로직 (중복 방지 & 애니메이션):
[+] 버튼으로 올릴 수 있는 최대 인원수는 불러온 명단의 총인원수를 넘지 못하게 막아줘.
[무작위 뽑기] 버튼을 누르면 약 2초 동안 학생 이름이 무작위로 빠르게 바뀌는 애니메이션(두구두구 효과)을 보여줘.
애니메이션이 끝나면 설정한 인원수만큼 절대 중복되지 않게 최종 학생들을 화면에 띄워줘. (배열을 섞은 뒤 자르는 방식을 써줘)
4. 효과음 (학교 방화벽 필수 고려):
최종 학생이 뽑히는 순간, 브라우저 내장 기능인 Web Audio API를 사용해서 '짠~!' 하고 올라가는 느낌의 주파수 효과음(사인파)을 만들어 줘. 학교 방화벽 때문에 외부 mp3나 ogg 링크는 절대 사용하면 안 돼.
전체 HTML 코드를 처음부터 끝까지 다 주지 말고, '발표 뽑기' 구역의 변경된 HTML과 하단에 추가할 JavaScript 코드만 딱 잘라서 제공해 줘.