<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Firebase 우리들의 방명록 (수정/삭제)</title>
<!-- Tailwind CSS CDN 로드 -->
<style>
/* 귀여운 글꼴 'Gamja Flower' 적용 */
body {
font-family: 'Gamja Flower', cursive;
}
/* 몽글몽글한 그라데이션 배경 애니메이션 */
@keyframes gradient-animation {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.animated-gradient {
background: linear-gradient(-45deg, #fce4ec, #f8bbd0, #e1f5fe, #b3e5fc);
background-size: 400% 400%;
animation: gradient-animation 15s ease infinite;
}
/* 메시지가 뿅 나타나는 애니메이션 */
@keyframes pop-in {
0% {
opacity: 0;
transform: translateY(20px) scale(0.9);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.message-pop-in {
animation: pop-in 0.5s ease-out forwards;
}
/* 스크롤바 디자인 */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #fce4ec; /* 연분홍 트랙 */
}
::-webkit-scrollbar-thumb {
background: #f48fb1; /* 핑크색 핸들 */
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #f06292;
}
</style>
</head>
<body class="animated-gradient flex items-center justify-center min-h-screen p-4">
<div class="w-full max-w-md mx-auto bg-white/80 backdrop-blur-sm rounded-3xl shadow-2xl p-6 sm:p-8 transition-all duration-500">
<header class="mb-6 text-center">
<h1 class="text-4xl font-bold text-pink-500">🏖️ 우리들의 방명록 ☀️</h1>
<p class="text-gray-600 text-lg mt-2">너의 이야기를 들려줘! 🍦✈️👙</p>
</header>
<!-- 메시지 표시 영역 -->
<main id="messages-container" class="h-80 overflow-y-auto bg-white/70 p-4 rounded-2xl mb-6 border border-pink-100 space-y-4"></main>
<!-- 메시지 입력 폼 -->
<form id="message-form" class="flex gap-3">
<input
id="nickname-input"
type="text"
placeholder="닉네임"
class="w-1/4 px-4 py-3 border-2 border-pink-200 bg-white/80 rounded-full focus:outline-none focus:ring-4 focus:ring-pink-300 focus:border-pink-400 transition text-gray-700 placeholder-gray-500"
required
>
<input
id="message-input"
type="text"
placeholder="어떤 이야기를 들려줄래?"
class="flex-1 px-4 py-3 border-2 border-pink-200 bg-white/80 rounded-full focus:outline-none focus:ring-4 focus:ring-pink-300 focus:border-pink-400 transition text-gray-700 placeholder-gray-500"
required
>
<button
type="submit"
class="bg-pink-500 text-white font-bold px-5 py-3 rounded-full hover:bg-pink-600 focus:outline-none focus:ring-4 focus:ring-pink-300 transition-all duration-300 transform hover:scale-110 active:scale-95"
>
전송 뿅!
</button>
</form>
<!-- 전체 삭제 버튼 추가 -->
<div class="mt-6 text-center">
<button id="clear-all-button" class="bg-red-500 text-white text-sm font-bold px-4 py-2 rounded-full hover:bg-red-600 transition-transform transform active:scale-95">
전체 초기화 🔥
</button>
</div>
</div>
<!-- Firebase SDK 스크립트 -->
<script type="module">
// Firebase v11 SDK 불러오기
// Firebase 프로젝트 설정
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID"
};
// Firebase 앱 및 서비스 초기화
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const auth = getAuth(app);
// UI 요소 가져오기
const messagesContainer = document.getElementById('messages-container');
const messageForm = document.getElementById('message-form');
const nicknameInput = document.getElementById('nickname-input');
const messageInput = document.getElementById('message-input');
const clearAllButton = document.getElementById('clear-all-button');
let currentUser = null;
// 사용자의 인증 상태 변경을 감지
onAuthStateChanged(auth, (user) => {
if (user) {
// 사용자가 (익명으로) 로그인한 경우
currentUser = user;
listenForMessages(); // 메시지 실시간 수신 시작
} else {
// 사용자가 로그아웃한 경우
currentUser = null;
messagesContainer.innerHTML = '<p class="text-center text-gray-500">로그인이 필요해요!</p>';
}
});
// 메시지를 실시간으로 가져와 화면에 표시하는 함수
function listenForMessages() {
const q = query(collection(db, "guestbook"));
onSnapshot(q, (querySnapshot) => {
messagesContainer.innerHTML = '';
if (querySnapshot.empty) {
messagesContainer.innerHTML = '<p class="text-center text-gray-500 text-lg">아직 아무도 글을 안썼어.. 힝 🥺<br>첫 번째 메시지를 남겨볼까?</p>';
return;
}
querySnapshot.forEach((docSnap) => {
const messageData = docSnap.data();
const messageId = docSnap.id;
const messageElement = document.createElement('div');
messageElement.classList.add('p-4', 'rounded-2xl', 'bg-gradient-to-br', 'from-sky-100', 'to-blue-200', 'text-gray-800', 'break-words', 'shadow-md', 'message-pop-in');
// 내가 쓴 글인지 확인하고, 맞다면 수정/삭제 버튼 추가
const isMyMessage = currentUser && messageData.uid === currentUser.uid;
const buttonsHTML = isMyMessage ? `
<div class="text-right mt-2">
<button class="edit-btn text-xs bg-yellow-400 text-white px-2 py-1 rounded-md hover:bg-yellow-500" data-id="${messageId}">수정</button>
<button class="delete-btn text-xs bg-red-400 text-white px-2 py-1 rounded-md hover:bg-red-500" data-id="${messageId}">삭제</button>
</div>
` : '';
messageElement.innerHTML = `
<p class="font-bold text-lg text-sky-800">${escapeHTML(messageData.nickname)} 👑</p>
<p class="message-text text-base mt-1">${escapeHTML(messageData.text)}</p>
<p class="text-xs text-right text-gray-500 mt-2">${new Date(messageData.timestamp).toLocaleString()}</p>
${buttonsHTML}
`;
messagesContainer.appendChild(messageElement);
});
messagesContainer.scrollTop = messagesContainer.scrollHeight;
});
}
// 메시지 전송 (폼 제출) 이벤트 처리
messageForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (!currentUser) {
alert('앗! 로그인이 필요해~');
return;
}
const nickname = nicknameInput.value;
const text = messageInput.value;
if (!nickname.trim() || !text.trim()) {
alert('앗! 닉네임과 메시지를 모두 적어줘~');
return;
}
try {
await addDoc(collection(db, "guestbook"), {
nickname: nickname,
text: text,
timestamp: Date.now(),
uid: currentUser.uid // 글쓴이의 고유 ID 저장
});
messageInput.value = '';
messageInput.focus();
} catch (error) {
console.error("메시지 추가 오류: ", error);
}
});
// 메시지 컨테이너에서 발생하는 클릭 이벤트를 처리 (이벤트 위임)
messagesContainer.addEventListener('click', async (e) => {
const docId = e.target.dataset.id;
if (!docId) return;
// 삭제 버튼을 클릭한 경우
if (e.target.classList.contains('delete-btn')) {
if (confirm('정말로 이 메시지를 삭제할까요?')) {
const docRef = doc(db, 'guestbook', docId);
await deleteDoc(docRef);
}
}
// 수정 버튼을 클릭한 경우
if (e.target.classList.contains('edit-btn')) {
const newText = prompt('메시지를 어떻게 수정할까요?');
if (newText !== null && newText.trim() !== '') {
const docRef = doc(db, 'guestbook', docId);
await updateDoc(docRef, { text: newText });
}
}
});
// [수정됨] 전체 초기화 버튼 클릭 시 비밀번호 확인
clearAllButton.addEventListener('click', async () => {
const password = prompt('관리자 비밀번호를 입력하세요.');
// 비밀번호가 맞는지 확인 (실제 앱에서는 더 안전한 방법을 사용해야 합니다)
if (password === '1234') {
if (confirm('⚠️ 정말로 모든 메시지를 삭제할까요? 이 작업은 되돌릴 수 없어요!')) {
const q = query(collection(db, "guestbook"));
const querySnapshot = await getDocs(q);
// 모든 문서를 병렬로 삭제하기 위한 프로미스 배열
const deletePromises = [];
querySnapshot.forEach((docSnap) => {
deletePromises.push(deleteDoc(doc(db, 'guestbook', docSnap.id)));
});
// 모든 삭제 작업이 완료될 때까지 기다림
await Promise.all(deletePromises);
alert('모든 메시지가 성공적으로 삭제되었습니다.');
}
} else if (password !== null) { // 사용자가 취소를 누르지 않았을 때만
alert('비밀번호가 틀렸습니다!');
}
});
// XSS(Cross-Site Scripting) 공격 방지를 위한 HTML 태그 치환 함수
function escapeHTML(str) {
const p = document.createElement("p");
p.appendChild(document.createTextNode(str));
return p.innerHTML;
}
// 앱 시작 시 익명으로 로그인
signInAnonymously(auth).catch((error) => {
console.error("익명 로그인 실패:", error);
});
</script>
</body>
</html>