<!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=Jua&family=Nanum+Pen+Script&family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet">
<style>
body {
/* 기본 폰트를 동글동글하고 귀여운 폰트로 설정 */
font-family: 'Jua', 'Noto Sans KR', sans-serif;
/* 편안한 자연 배경 애니메이션 (초원 느낌) */
background: linear-gradient(-45deg, #a8edea, #fed6e3, #e0c3fc, #8ec5fc);
background-size: 400% 400%;
animation: gradientBG 15s ease infinite;
margin: 0;
overflow-x: hidden;
}
@keyframes gradientBG {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* 글래스모피즘 (유리 효과) 패널 */
.glass-panel {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.8);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
}
/* 둥실둥실 애니메이션 */
.float-anim {
animation: floating 3s ease-in-out infinite;
}
@keyframes floating {
0% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
100% { transform: translateY(0px); }
}
/* 감정 버튼 호버 효과 */
.emotion-btn {
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.emotion-btn:hover {
transform: scale(1.1) translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
/* 학생 버튼 호버 효과 */
.student-btn {
transition: all 0.2s;
}
.student-btn:hover {
transform: scale(1.05);
background-color: #fef08a; /* 노란색 하이라이트 */
color: #854d0e;
}
/* 완료 화면 효과 */
.success-overlay {
background: radial-gradient(circle, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.8) 100%);
}
.hide { display: none !important; }
</style>
</head>
<body class="min-h-screen flex flex-col items-center justify-center p-4 md:p-8">
<audio id="bgm-forest" loop src="https://actions.google.com/sounds/v1/ambiences/forest_morning.ogg"></audio>
<audio id="bgm-ocean" loop src="https://actions.google.com/sounds/v1/water/waves_crashing_on_rock_beach.ogg"></audio>
<div class="fixed top-4 right-4 flex gap-2 z-50">
<button id="btn-forest" class="glass-panel p-3 rounded-full text-2xl hover:bg-white transition-colors" title="산새 소리 듣기">🌲</button>
<button id="btn-ocean" class="glass-panel p-3 rounded-full text-2xl hover:bg-white transition-colors" title="파도 소리 듣기">🌊</button>
<button id="btn-mute" class="glass-panel p-3 rounded-full text-2xl hover:bg-white transition-colors bg-red-100" title="소리 끄기">🔇</button>
</div>
<button id="btn-settings" class="fixed top-4 left-4 glass-panel px-4 py-2 rounded-full text-gray-700 hover:bg-white transition-colors font-sans font-bold flex items-center gap-2 z-50">
⚙️ 명단 설정
</button>
<div class="glass-panel w-full max-w-5xl rounded-3xl p-6 md:p-10 relative overflow-hidden flex flex-col items-center min-h-[600px]">
<div class="w-32 h-24 mb-2 flex items-center justify-center drop-shadow-md">
<img src="https://i.ibb.co/0RrYkTgr/logo2.png" alt="스쿨앱 로고" class="w-full h-full object-contain">
</div>
<div class="text-center mb-8">
<h1 class="text-4xl md:text-5xl text-blue-800 drop-shadow-sm mb-2">오늘의 마음 날씨 🌈</h1>
<p id="subtitle" class="text-xl md:text-2xl text-gray-600 font-sans font-medium">안녕! 네 이름표를 찾아 눌러줄래?</p>
</div>
<div id="screen-students" class="w-full flex-1 flex flex-col items-center w-full">
<div id="student-grid" class="grid grid-cols-3 md:grid-cols-5 lg:grid-cols-6 gap-3 md:gap-4 w-full">
</div>
</div>
<div id="screen-emotions" class="w-full flex-1 flex flex-col items-center hide w-full">
<div class="bg-white/80 px-8 py-3 rounded-full mb-8 shadow-sm">
<span id="selected-student-name" class="text-3xl text-indigo-600">김철수</span>
<span class="text-2xl text-gray-700">, 지금 기분이 어때?</span>
</div>
<div id="emotion-grid" class="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4 w-full">
</div>
<button id="btn-back" class="mt-12 px-6 py-2 bg-gray-400 hover:bg-gray-500 text-white rounded-full font-sans font-bold transition-colors">
돌아가기
</button>
</div>
<div id="screen-success" class="absolute inset-0 success-overlay flex flex-col items-center justify-center hide z-40">
<div class="text-8xl mb-6 float-anim">💖</div>
<h2 class="text-4xl md:text-5xl text-pink-600 mb-4 text-center">알려줘서 고마워!</h2>
<p id="success-msg" class="text-2xl text-gray-700 text-center font-sans">오늘도 행복한 하루 보내렴 😊</p>
</div>
<div id="screen-settings" class="absolute inset-0 glass-panel flex flex-col items-center justify-center hide z-50 p-6 bg-white/90">
<h2 class="text-3xl text-gray-800 mb-4">학생 명단 입력</h2>
<p class="text-gray-600 mb-4 font-sans text-center">엑셀이나 구글 시트에서 학생 이름을 복사해서 붙여넣으세요.<br>줄바꿈으로 구분됩니다.</p>
<textarea id="student-input" class="w-full max-w-md h-64 p-4 border-2 border-blue-300 rounded-xl font-sans text-lg focus:outline-none focus:border-blue-500" placeholder="김철수 이영희 박지민..."></textarea>
<div class="flex flex-wrap justify-center gap-3 mt-6">
<button id="btn-fetch-settings" class="px-6 py-3 bg-green-500 hover:bg-green-600 text-white rounded-xl font-sans font-bold text-lg transition-colors flex items-center gap-2 shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
시트에서 가져오기
</button>
<button id="btn-save-settings" class="px-8 py-3 bg-blue-500 hover:bg-blue-600 text-white rounded-xl font-sans font-bold text-lg transition-colors shadow-sm">저장하기</button>
<button id="btn-cancel-settings" class="px-8 py-3 bg-gray-400 hover:bg-gray-500 text-white rounded-xl font-sans font-bold text-lg transition-colors shadow-sm">닫기</button>
</div>
<div class="mt-8 p-4 bg-yellow-50 rounded-lg max-w-md w-full border border-yellow-200">
<h3 class="font-bold text-yellow-800 mb-2 font-sans">💡 향후 구글 시트 자동 연동 안내</h3>
<p class="text-sm text-yellow-700 font-sans">현재 버전은 브라우저에 임시 저장됩니다. 실제 학급 운영 시에는 선생님의 구글 시트에 <b>'Apps Script'</b>를 추가하여 앱과 통신하게 만들거나, 앱 내 데이터베이스를 추가하여 <b>엑셀 다운로드</b> 기능을 추가할 수 있습니다.</p>
</div>
</div>
</div>
<div id="toast" class="fixed bottom-10 bg-gray-800 text-white px-6 py-3 rounded-full font-sans transition-opacity duration-300 opacity-0 pointer-events-none z-50">
메시지
</div>
<div id="loading-overlay" class="fixed inset-0 bg-white/50 backdrop-blur-sm flex flex-col items-center justify-center z-[60] hide">
<div class="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-blue-500 mb-4"></div>
<p class="text-xl font-bold text-gray-700 font-sans">마음을 구름에 싣는 중...</p>
</div>
<script>
// --- 구글 시트 API URL 설정 ---
// ★★★ 새로운 웹 앱 URL 반영 완료 ★★★
const GOOGLE_SCRIPT_URL = "https://script.google.com/macros/s/AKfycbyKOTosw1zaPBfU-EFF5JnhP_JE37QqxR92skiUp55ooghud1BWBuJAIo31BfG_hdOD7A/exec";
// --- 데이터 정의 ---
// 기본 학생 명단 (초기값 비워두기)
let students = [];
// 18가지 감정 데이터 (긍정, 중립, 부정 골고루 배치)
const emotions = [
// 긍정적 감정
{ id: 'happy', emoji: '😊', name: '행복해요', color: 'bg-yellow-100 hover:bg-yellow-200 border-yellow-300' },
{ id: 'excited', emoji: '🤩', name: '신나요', color: 'bg-yellow-100 hover:bg-yellow-200 border-yellow-300' },
{ id: 'calm', emoji: '😌', name: '평온해요', color: 'bg-green-100 hover:bg-green-200 border-green-300' },
{ id: 'proud', emoji: '🌟', name: '자랑스러워요', color: 'bg-orange-100 hover:bg-orange-200 border-orange-300' },
{ id: 'grateful', emoji: '🙏', name: '감사해요', color: 'bg-emerald-100 hover:bg-emerald-200 border-emerald-300' },
{ id: 'refreshed', emoji: '🍃', name: '상쾌해요', color: 'bg-teal-100 hover:bg-teal-200 border-teal-300' },
// 중립적 감정
{ id: 'okay', emoji: '😐', name: '그저그래요', color: 'bg-gray-100 hover:bg-gray-200 border-gray-300' },
{ id: 'tired', emoji: '😫', name: '피곤해요', color: 'bg-stone-100 hover:bg-stone-200 border-stone-300' },
{ id: 'sleepy', emoji: '🥱', name: '졸려요', color: 'bg-slate-100 hover:bg-slate-200 border-slate-300' },
{ id: 'blank', emoji: '😶', name: '멍해요', color: 'bg-zinc-100 hover:bg-zinc-200 border-zinc-300' },
{ id: 'relaxed', emoji: '🛋️', name: '편안해요', color: 'bg-blue-50 hover:bg-blue-100 border-blue-200' },
{ id: 'hopeful', emoji: '🥺', name: '기대돼요', color: 'bg-fuchsia-100 hover:bg-fuchsia-200 border-fuchsia-300' },
// 부정적/도움이 필요한 감정
{ id: 'sad', emoji: '😢', name: '슬퍼요', color: 'bg-blue-100 hover:bg-blue-200 border-blue-300' },
{ id: 'angry', emoji: '😡', name: '화나요', color: 'bg-red-100 hover:bg-red-200 border-red-300' },
{ id: 'anxious', emoji: '😰', name: '불안해요', color: 'bg-purple-100 hover:bg-purple-200 border-purple-300' },
{ id: 'depressed', emoji: '🌧️', name: '우울해요', color: 'bg-indigo-100 hover:bg-indigo-200 border-indigo-300' },
{ id: 'annoyed', emoji: '😒', name: '짜증나요', color: 'bg-rose-100 hover:bg-rose-200 border-rose-300' },
{ id: 'sick', emoji: '🤒', name: '아파요', color: 'bg-cyan-100 hover:bg-cyan-200 border-cyan-300' }
];
// 앱 상태 변수
let currentStudent = null;
let attendanceRecord = {}; // 임시 저장소: { "김철수": { "2023-10-25": "행복해요" } }
// --- DOM 요소 ---
const screenStudents = document.getElementById('screen-students');
const screenEmotions = document.getElementById('screen-emotions');
const screenSuccess = document.getElementById('screen-success');
const screenSettings = document.getElementById('screen-settings');
const subtitle = document.getElementById('subtitle');
const studentGrid = document.getElementById('student-grid');
const emotionGrid = document.getElementById('emotion-grid');
const selectedStudentName = document.getElementById('selected-student-name');
// --- 초기화 및 렌더링 ---
function initApp() {
// 로컬 스토리지에서 명단 불러오기 (있으면)
const savedStudents = localStorage.getItem('emotionAppStudents');
if (savedStudents) {
students = JSON.parse(savedStudents);
document.getElementById('student-input').value = students.join('\n');
} else {
document.getElementById('student-input').value = students.join('\n');
}
renderStudents();
renderEmotions();
}
function renderStudents() {
studentGrid.innerHTML = '';
// 학생 명단이 없을 경우 안내 메시지 출력
if (students.length === 0) {
studentGrid.innerHTML = `
<div class="col-span-full flex flex-col items-center justify-center py-12 text-gray-500">
<span class="text-4xl mb-4">📝</span>
<p class="text-xl font-sans text-center">좌측 상단의 <b>[⚙️ 명단 설정]</b> 버튼을 눌러<br>학생들의 이름을 먼저 등록해주세요.</p>
</div>
`;
return;
}
// 오늘 날짜 문자열 (예: "2023-10-25")
const today = new Date().toISOString().split('T')[0];
students.forEach(student => {
const btn = document.createElement('button');
// 이미 오늘 기록을 남겼는지 확인
const hasRecordedToday = attendanceRecord[student] && attendanceRecord[student][today];
btn.className = `student-btn bg-white/90 border-2 border-white rounded-2xl py-4 px-2 text-xl md:text-2xl font-bold shadow-sm flex flex-col items-center justify-center gap-1 ${hasRecordedToday ? 'opacity-50 grayscale' : ''}`;
// 학생 이름만 렌더링 (번호 제거)
btn.innerHTML = `<span>${student}</span>`;
btn.onclick = () => {
if (hasRecordedToday) {
showToast(`${student} 친구는 오늘 이미 마음을 알려줬어요!`);
}
selectStudent(student, student);
};
studentGrid.appendChild(btn);
});
}
function renderEmotions() {
emotionGrid.innerHTML = '';
emotions.forEach(emo => {
const btn = document.createElement('button');
btn.className = `emotion-btn ${emo.color} border-4 rounded-3xl p-4 flex flex-col items-center justify-center gap-2 shadow-sm cursor-pointer`;
btn.innerHTML = `
<span class="text-5xl md:text-6xl mb-1">${emo.emoji}</span>
<span class="text-xl md:text-2xl text-gray-800">${emo.name}</span>
`;
btn.onclick = () => selectEmotion(emo);
emotionGrid.appendChild(btn);
});
}
// --- 기능 로직 ---
function selectStudent(fullName, displayName) {
currentStudent = fullName;
selectedStudentName.textContent = displayName;
// 화면 전환
screenStudents.classList.add('hide');
screenEmotions.classList.remove('hide');
subtitle.textContent = "가장 비슷한 마음을 골라봐!";
window.scrollTo({ top: 0, behavior: 'smooth' });
}
async function selectEmotion(emotionObj) {
const today = new Date().toISOString().split('T')[0];
// 1. 임시 기록 저장 (로컬)
if(!attendanceRecord[currentStudent]) {
attendanceRecord[currentStudent] = {};
}
attendanceRecord[currentStudent][today] = emotionObj.name;
// 2. 구글 시트로 데이터 전송 (API 호출)
if (GOOGLE_SCRIPT_URL && GOOGLE_SCRIPT_URL !== "") {
const loadingOverlay = document.getElementById('loading-overlay');
loadingOverlay.querySelector('p').textContent = "마음을 구름에 싣는 중...";
loadingOverlay.classList.remove('hide'); // 로딩 시작
try {
const response = await fetch(GOOGLE_SCRIPT_URL, {
method: 'POST',
mode: 'no-cors', // CORS 우회를 위해 no-cors 사용 (이 경우 응답 내용을 읽을 순 없음)
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: currentStudent,
emotion: emotionObj.name
})
});
// no-cors 모드이므로 정상 응답 여부를 정확히 알 순 없지만 오류 없이 통과하면 성공으로 간주
console.log(`[구글 시트 전송 시도 완료] 이름: ${currentStudent}, 감정: ${emotionObj.name}`);
} catch (error) {
console.error("구글 시트 저장 오류:", error);
showToast("기록 저장에 문제가 발생했습니다. (네트워크 확인 필요)");
} finally {
loadingOverlay.classList.add('hide'); // 로딩 종료
}
} else {
console.log(`[로컬 기록됨] 이름: ${currentStudent}, 감정: ${emotionObj.name} (구글 시트 URL이 설정되지 않음)`);
}
// 선생님을 위한 맞춤 메시지 준비
const msgObj = document.getElementById('success-msg');
if (['슬퍼요', '화나요', '불안해요', '우울해요', '짜증나요', '아파요'].includes(emotionObj.name)) {
msgObj.textContent = "그랬구나, 이따가 선생님이랑 잠깐 이야기 나눌까? 토닥토닥 💖";
msgObj.className = "text-2xl text-blue-700 text-center font-sans mt-2 bg-blue-100 py-2 px-6 rounded-full";
} else {
msgObj.textContent = "오늘도 행복하고 즐거운 하루 보내렴! 😊";
msgObj.className = "text-2xl text-gray-700 text-center font-sans mt-2";
}
// 완료 화면 표시
screenEmotions.classList.add('hide');
screenSuccess.classList.remove('hide');
subtitle.textContent = "참 잘했어요!";
// 3초 후 초기 화면으로 복귀
setTimeout(() => {
screenSuccess.classList.add('hide');
screenStudents.classList.remove('hide');
subtitle.textContent = "안녕! 네 이름표를 찾아 눌러줄래?";
currentStudent = null;
renderStudents(); // 명단 다시 그려서 완료 처리된 버튼 갱신
}, 3000);
}
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.classList.remove('opacity-0');
setTimeout(() => {
toast.classList.add('opacity-0');
}, 2500);
}
// --- 이벤트 리스너 ---
// 돌아가기 버튼
document.getElementById('btn-back').addEventListener('click', () => {
screenEmotions.classList.add('hide');
screenStudents.classList.remove('hide');
subtitle.textContent = "안녕! 네 이름표를 찾아 눌러줄래?";
currentStudent = null;
});
// 명단 설정 버튼
document.getElementById('btn-settings').addEventListener('click', () => {
screenSettings.classList.remove('hide');
});
document.getElementById('btn-cancel-settings').addEventListener('click', () => {
screenSettings.classList.add('hide');
});
document.getElementById('btn-save-settings').addEventListener('click', () => {
const input = document.getElementById('student-input').value;
const newStudents = input.split('\n').map(s => s.trim()).filter(s => s.length > 0);
if (newStudents.length > 0) {
students = newStudents;
localStorage.setItem('emotionAppStudents', JSON.stringify(students));
renderStudents();
screenSettings.classList.add('hide');
showToast("명단이 저장되었습니다.");
} else {
alert("명단을 1명 이상 입력해주세요.");
}
});
// 명단 시트에서 가져오기 버튼 로직
document.getElementById('btn-fetch-settings').addEventListener('click', async () => {
if (!GOOGLE_SCRIPT_URL || GOOGLE_SCRIPT_URL === "") {
alert("구글 시트 URL이 설정되지 않았습니다.");
return;
}
const loadingOverlay = document.getElementById('loading-overlay');
const loadingText = loadingOverlay.querySelector('p');
const originalText = loadingText.textContent;
loadingText.textContent = "시트에서 명단을 불러오는 중...";
loadingOverlay.classList.remove('hide');
try {
const response = await fetch(GOOGLE_SCRIPT_URL);
const result = await response.json();
if (result.status === "success" && result.data && result.data.length > 0) {
students = result.data;
document.getElementById('student-input').value = students.join('\n');
localStorage.setItem('emotionAppStudents', JSON.stringify(students));
renderStudents();
screenSettings.classList.add('hide');
showToast(`총 ${students.length}명의 명단을 성공적으로 가져왔습니다!`);
} else {
alert("명단을 가져오는데 실패했거나 시트에 이름이 없습니다.");
}
} catch (error) {
console.error("명단 가져오기 오류:", error);
alert("명단을 가져오지 못했습니다. 앱스 스크립트를 최신 버전으로 새로 배포했는지 확인해주세요.");
} finally {
loadingText.textContent = originalText; // 텍스트 복구
loadingOverlay.classList.add('hide');
}
});
// 오디오 컨트롤 로직
const audioForest = document.getElementById('bgm-forest');
const audioOcean = document.getElementById('bgm-ocean');
const btnForest = document.getElementById('btn-forest');
const btnOcean = document.getElementById('btn-ocean');
const btnMute = document.getElementById('btn-mute');
function stopAllAudio() {
audioForest.pause();
audioOcean.pause();
btnForest.classList.remove('bg-green-200');
btnOcean.classList.remove('bg-blue-200');
btnMute.classList.remove('bg-red-200');
}
btnForest.addEventListener('click', () => {
stopAllAudio();
audioForest.play().catch(e => console.log("자동재생 방지됨"));
btnForest.classList.add('bg-green-200');
});
btnOcean.addEventListener('click', () => {
stopAllAudio();
audioOcean.play().catch(e => console.log("자동재생 방지됨"));
btnOcean.classList.add('bg-blue-200');
});
btnMute.addEventListener('click', () => {
stopAllAudio();
btnMute.classList.add('bg-red-200');
});
// 앱 시작
initApp();
</script>
</body>
</html>
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
// 1. 앱에서 '명단 가져오기'를 눌렀을 때 명단을 보내주는 함수
function doGet(e) {
try {
const dataRange = sheet.getRange("A2:A"); // A2부터 A열 전체 (A1은 '이름' 등 헤더)
const values = dataRange.getDisplayValues();
const students = [];
// 빈칸을 제외하고 이름만 쏙쏙 뽑아내기
for (let i = 0; i < values.length; i++) {
if (values[i][0].trim() !== "") {
students.push(values[i][0].trim());
}
}
return ContentService.createTextOutput(JSON.stringify({ "status": "success", "data": students }))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService.createTextOutput(JSON.stringify({ "status": "error", "message": error.message }))
.setMimeType(ContentService.MimeType.JSON);
}
}
// 2. 앱에서 '감정'을 눌렀을 때 기록하는 함수
function doPost(e) {
try {
const data = JSON.parse(e.postData.contents);
const studentName = data.name;
const emotion = data.emotion;
const today = new Date();
const dateString = Utilities.formatDate(today, Session.getScriptTimeZone(), "yyyy-MM-dd");
const dataRange = sheet.getDataRange();
const displayValues = dataRange.getDisplayValues();
let dateColIndex = -1;
const headerRow = displayValues[0];
for (let i = 0; i < headerRow.length; i++) {
if (headerRow[i] == dateString) {
dateColIndex = i + 1;
break;
}
}
if (dateColIndex === -1) {
dateColIndex = headerRow.length + 1;
sheet.getRange(1, dateColIndex).setValue(dateString);
sheet.getRange(1, dateColIndex).setNumberFormat("@");
}
let studentRowIndex = -1;
for (let i = 0; i < displayValues.length; i++) {
if (displayValues[i][0] == studentName) {
studentRowIndex = i + 1;
break;
}
}
if (studentRowIndex === -1) {
studentRowIndex = displayValues.length + 1;
sheet.getRange(studentRowIndex, 1).setValue(studentName);
}
sheet.getRange(studentRowIndex, dateColIndex).setValue(emotion);
return ContentService.createTextOutput(JSON.stringify({ "status": "success", "message": "기록 성공" })).setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService.createTextOutput(JSON.stringify({ "status": "error", "message": error.message })).setMimeType(ContentService.MimeType.JSON);
}
}
function doOptions(e) {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
};
return ContentService.createTextOutput("OK").setMimeType(ContentService.MimeType.TEXT);
}
1. 앱 개요
목적: 학생들이 아침에 등교하여 스스로 자신의 감정을 돌아보고 표현할 수 있도록 돕는 감정 기록 플랫폼입니다.
기술 스택: HTML5, CSS3 (Tailwind CSS 활용), JavaScript (단일 HTML 파일 구성)
데이터 연동: Google Apps Script(웹 앱)를 이용한 Google SpreadSheet 실시간 데이터 저장 및 불러오기
2. UI/UX 디자인 (전체 테마)
비주얼 컨셉: 아이들이 심리적 안정감을 느낄 수 있는 '동화 같고 편안한 자연' 테마.
배경: 파스텔톤(민트, 연분홍, 연보라, 하늘색)이 천천히 교차하며 움직이는 애니메이션 그라데이션 배경 적용.
폰트: 동글동글하고 귀여운 폰트(Jua) 메인 사용, 가독성이 필요한 곳은 고딕 계열(Noto Sans KR) 혼용.
패널 효과: 중앙의 메인 컨텐츠 영역은 반투명한 유리 질감 효과(Glassmorphism)를 적용하여 부드러운 느낌 강조.
3. 핵심 요소 및 화면 구성
로고 및 타이틀: 상단 중앙에 학교/앱 로고 이미지(https://i.ibb.co/0RrYkTgr/logo2.png) 배치. 아래에 "오늘의 마음 날씨 🌈" 타이틀과 부제목 표시.
배경음악 컨트롤러 (우측 상단):
🌲 산새 소리, 🌊 파도 소리를 선택해서 들을 수 있는 버튼 제공 (구글 무료 효과음 링크 활용).
🔇 소리 끄기 버튼 제공.
설정 버튼 (좌측 상단): '⚙️ 명단 설정' 버튼을 눌러 학생 명단을 관리할 수 있는 모달(레이어) 창 띄우기.
4. 상세 기능 (화면 흐름)
STEP 1. 명단 설정 화면 (선생님용)
화면 구성: 안내 문구, 명단이 표시될 텍스트 에리어(Textarea), 컨트롤 버튼들.
기능 1 (시트에서 가져오기): 버튼 클릭 시 Google Apps Script의 doGet API를 호출하여 시트의 A열(이름) 데이터를 가져와 텍스트 에리어에 채움. (로딩 스피너 UI 표시)
기능 2 (저장 및 직접 입력): 텍스트 에리어에 엔터(줄바꿈)로 구분된 이름을 입력/수정 후 '저장'을 누르면 로컬 스토리지(localStorage)에 배열 형태로 저장되고 화면이 갱신됨.
STEP 2. 학생 선택 화면 (초기 화면)
등록된 학생 명단(이름)을 그리드(Grid) 형태의 버튼으로 나열함. 버튼은 마우스 호버 시 살짝 커지며 노란색으로 강조됨.
오늘 기록 여부 체크: 앱이 로드될 때 또는 학생을 선택할 때, 이미 오늘 감정을 기록한 학생의 버튼은 흑백/반투명 처리(opacity-50 grayscale)하여 시각적으로 구분함.
학생 버튼을 클릭하면 STEP 3 화면으로 부드럽게 전환.
STEP 3. 감정 선택 화면
상단에 선택한 학생의 이름과 함께 "OOO, 지금 기분이 어때?" 메시지 표시.
감정 버튼 (18가지): 긍정, 중립, 부정(도움이 필요한) 감정을 골고루 배치. 각 버튼은 이모지(Emoji)와 텍스트(예: 😊 행복해요)로 구성되며, 감정 성격에 맞는 배경색(예: 긍정=노랑/초록, 부정=빨강/파랑)을 가짐.
'돌아가기' 버튼을 제공하여 학생 선택 화면으로 되돌아갈 수 있음.
STEP 4. 기록 저장 및 완료 오버레이
학생이 감정을 선택하면 즉시 화면 전체를 덮는 반투명 완료 오버레이("💖 알려줘서 고마워!")가 나타남.
자동 피드백: * 부정적인 감정('슬퍼요', '화나요', '아파요' 등) 선택 시 -> "그랬구나, 이따가 선생님이랑 잠깐 이야기 나눌까? 토닥토닥 💖"
긍정/중립 감정 선택 시 -> "오늘도 행복하고 즐거운 하루 보내렴! 😊"
3초 후 완료 화면이 자동으로 닫히고, 초기(학생 선택) 화면으로 돌아감.
5. 데이터베이스 (Google Sheets) 연동 명세
API 호출: JavaScript의 fetch 함수를 사용하여 GOOGLE_SCRIPT_URL로 HTTP 요청을 보냄. (보안 정책 우회를 위해 mode: 'no-cors' 사용)
데이터 전송 (POST): 학생이 감정을 선택할 때 { name: "학생이름", emotion: "감정이름" } 형태의 JSON 데이터를 전송함.
데이터 수신 (GET): '시트에서 가져오기' 버튼 클릭 시 JSON 형태로 명단 배열을 수신함.
Apps Script 요구 로직 (doPost):
오늘 날짜를 yyyy-MM-dd 형식의 문자열로 생성.
시트의 첫 번째 행(헤더)을 검색하여 오늘 날짜와 일치하는 열(Column)을 찾음. 없으면 맨 끝에 새 열을 생성하고 날짜를 입력 (자동 서식 변환을 막기 위해 텍스트 형식@으로 강제 설정).
시트의 A열을 검색하여 입력받은 학생 이름의 행(Row)을 찾음. 없으면 맨 아래에 새 행을 추가하여 이름 입력.
찾아낸 (또는 생성한) 해당 행과 열의 교차 지점(셀)에 감정 텍스트를 기록함.
이 프롬프트를 바탕으로 앱을 요청하시면, 정확히 의도하신 구조와 따뜻한 감성을 지닌 출석부가 만들어질 것입니다!