데이터베이스

[Firebase] 4장 FirebaseStorage를 활용한 파일 업로드 실습

파아란기쁨1 2025. 8. 23. 09:29
반응형

🎠 Firebase Storage 이미지 업로드 실습 가이드

📚 4장: Firebase Storage를 활용한 파일 업로드

안녕하세요! 이번 시간에는 우리가 3장에서 만든 메시지 게시판에 이미지 첨부 기능을 추가해볼 거예요! 마치 놀이공원에서 사진을 찍고 전시하는 것처럼 말이에요! 📸

🎯 이번 시간에 배울 내용

  1. Firebase Storage 설정하기 - 이미지를 저장할 창고 만들기
  2. 이미지 파일 선택하기 - 컴퓨터에서 사진 고르기
  3. 이미지 업로드하기 - 사진을 클라우드에 올리기
  4. 업로드 진행률 보여주기 - 얼마나 올라갔는지 확인하기
  5. 이미지 다운로드 및 표시하기 - 올린 사진을 다시 보기
  6. 이미지와 함께 메시지 저장하기 - 사진과 글을 함께 저장하기

🔧 주요 기능 설명

1. 이미지 선택 기능

  • 파일 선택 버튼: 컴퓨터에서 이미지 파일을 선택할 수 있어요
  • 미리보기: 선택한 이미지를 바로 확인할 수 있어요
  • 파일 형식: JPG, PNG, GIF 등 이미지 파일만 선택 가능해요

2. 업로드 진행률 표시

  • 진행바: 이미지가 얼마나 업로드되었는지 시각적으로 보여줘요
  • 퍼센트 표시: 0%부터 100%까지 숫자로도 확인할 수 있어요
  • 상태 메시지: "업로드 중...", "업로드 완료!" 같은 메시지로 상황을 알려줘요

3. 이미지 저장 및 표시

  • Firebase Storage: Google의 클라우드 저장소에 이미지를 안전하게 보관해요
  • 다운로드 URL: 업로드된 이미지의 주소를 받아와서 화면에 보여줘요
  • 이미지 클릭: 작은 이미지를 클릭하면 새 창에서 크게 볼 수 있어요

🎪 코드 핵심 포인트

이미지 업로드 함수

 
 
javascript
// 이미지를 Firebase Storage에 올리는 마법의 함수!
async function uploadImage(file) {
    const fileName = Date.now() + '_' + file.name;  // 특별한 이름 만들기
    const storageRef = storage.ref('images/' + fileName);  // 저장 위치 정하기
    const uploadTask = storageRef.put(file);  // 업로드 시작!
    
    // 진행률을 보여주면서 업로드하기
    uploadTask.on('state_changed', 
        function(snapshot) {
            const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
            // 진행바 업데이트하기
        }
    );
}

🚀 실습 단계

1단계: Firebase Storage 활성화

  1. Firebase Console에서 프로젝트 선택
  2. "Storage" 메뉴 클릭
  3. "시작하기" 버튼 클릭
  4. 보안 규칙 설정 (테스트 모드로 시작)

2단계: HTML 파일에 설정 추가

 
 
html
<script src="https://cdnjs.cloudflare.com/ajax/libs/firebase/9.15.0/firebase-storage-compat.min.js"></script>

3단계: JavaScript에서 Storage 초기화

 
 
javascript
const storage = firebase.storage();

 

4단계: 이미지 업로드 기능 테스트

  1. 이미지 파일 선택 버튼 클릭
  2. 컴퓨터에서 사진 파일 선택
  3. 미리보기에서 선택한 이미지 확인
  4. "메시지 게시하기" 버튼 클릭
  5. 진행바로 업로드 상태 확인
  6. 메시지 보드에서 이미지와 함께 표시된 메시지 확인

🎨 UI/UX 특징

시각적 피드백

  • 진행바: 파란색에서 초록색으로 변하는 그라데이션으로 진행률 표시
  • 버튼 상태: 업로드 중에는 버튼이 비활성화되어 중복 클릭 방지
  • 호버 효과: 마우스를 올리면 버튼과 카드가 살짝 커져서 반응성 제공

사용자 친화적 기능

  • 드래그 앤 드롭 스타일: 점선 테두리로 파일을 끌어다 놓을 수 있는 느낌
  • 미리보기: 업로드 전에 선택한 이미지를 미리 확인 가능
  • 이미지 클릭 확대: 작은 이미지를 클릭하면 새 탭에서 원본 크기로 보기

🔍 코드 상세 분석

1. 파일 선택 감지

 
 
javascript
document.getElementById('imageInput').addEventListener('change', function(e) {
    const file = e.target.files[0];  // 선택한 첫 번째 파일
    
    if (file) {
        selectedFile = file;  // 전역 변수에 저장
        
        // FileReader로 미리보기 만들기
        const reader = new FileReader();
        reader.onload = function(e) {
            // 선택한 이미지를 화면에 보여주기
        };
        reader.readAsDataURL(file);
    }
});

2. 업로드 진행률 추적

 
 
javascript
uploadTask.on('state_changed', 
    function(snapshot) {
        // 진행률 계산: (업로드된 바이트 / 전체 바이트) × 100
        const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
        
        // 진행바 너비 업데이트
        progressFill.style.width = progress + '%';
        progressText.textContent = `업로드 중... ${Math.round(progress)}%`;
    }
);

3. 이미지 URL 저장

 
 
javascript
// Firestore에 저장할 데이터
const messageData = {
    author: author,
    message: message,
    timestamp: firebase.firestore.FieldValue.serverTimestamp(),
    imageUrl: imageUrl  // 업로드된 이미지의 다운로드 URL
};

🛡️ 보안 및 최적화 팁

Firebase Storage 보안 규칙 (초보자용)

 
 
javascript
// 테스트 모드 (누구나 읽기/쓰기 가능)
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if true;  // 나중에 인증 기능 추가 시 수정 필요
    }
  }
}

파일 크기 제한

 
 
javascript
// 파일 크기 체크 (5MB 제한)
if (file.size > 5 * 1024 * 1024) {
    alert('파일 크기는 5MB 이하로 선택해주세요! 🙏');
    return;
}

이미지 형식 확인

 
 
javascript
// 이미지 파일만 허용
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
    alert('이미지 파일만 업로드 할 수 있어요! (JPG, PNG, GIF, WebP) 📸');
    return;
}

🚨 자주 발생하는 문제와 해결법

1. 이미지가 안 보여요!

원인: Firebase Storage 보안 규칙이나 CORS 설정 문제 해결법:

  • Firebase Console에서 Storage 보안 규칙 확인
  • 브라우저 개발자 도구에서 에러 메시지 확인

2. 업로드가 너무 느려요!

원인: 이미지 파일이 너무 큰 경우 해결법:

  • 이미지 크기 제한 추가 (예: 5MB)
  • 이미지 압축 라이브러리 사용 고려

3. 진행바가 안 움직여요!

원인: 업로드 이벤트 리스너가 제대로 설정되지 않음 해결법:

  • uploadTask.on('state_changed', ...) 부분 다시 확인
  • 콘솔에서 에러 메시지 확인

🎯 다음 단계 학습 방향

5장 예고: Firebase Authentication

  • 사용자 로그인/회원가입 기능
  • 본인이 작성한 글만 수정/삭제하기
  • 사용자 프로필 이미지 기능

추가 기능 아이디어

  1. 이미지 여러 개 업로드: 한 번에 여러 장의 사진 첨부
  2. 이미지 필터: 업로드 전에 사진에 필터 효과 적용
  3. 이미지 리사이징: 자동으로 적절한 크기로 조정
  4. 이미지 갤러리: 업로드된 모든 이미지를 갤러리 형태로 보기

💡 쉬운 설명

"여러분! 이번에는 우리 놀이공원 게시판에 사진관을 만들었어요! 📸

  1. 사진 선택하기: 컴퓨터에 있는 예쁜 사진을 고를 수 있어요
  2. 사진 올리기: 선택한 사진을 인터넷 구름(클라우드) 위로 올려보내요
  3. 진행률 보기: 사진이 얼마나 올라갔는지 막대기로 볼 수 있어요 (0%부터 100%까지!)
  4. 사진 보기: 올린 사진을 다른 친구들도 볼 수 있어요
  5. 사진 크게 보기: 작은 사진을 클릭하면 크게도 볼 수 있어요!

마치 놀이공원에서 재미있는 순간을 사진으로 찍고, 친구들에게 보여주는 것 같아요! 🎠✨"

🎪 실습 완료 체크리스트

  • Firebase Storage가 프로젝트에 추가되었나요?
  • 이미지 파일을 선택할 수 있나요?
  • 선택한 이미지가 미리보기로 보이나요?
  • 업로드 진행률이 진행바로 표시되나요?
  • 업로드가 완료되면 메시지와 함께 이미지가 보이나요?
  • 이미지를 클릭하면 새 창에서 크게 볼 수 있나요?
  • 이미지가 포함된 메시지도 수정/삭제가 되나요?

 

전체 코드

<!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://cdnjs.cloudflare.com/ajax/libs/firebase/9.15.0/firebase-app-compat.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/firebase/9.15.0/firebase-firestore-compat.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/firebase/9.15.0/firebase-storage-compat.min.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Comic Sans MS', cursive, sans-serif;
            background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 50%, #fecfef 100%);
            min-height: 100vh;
            padding: 20px;
        }

        .playground-container {
            max-width: 800px;
            margin: 0 auto;
            background: rgba(255, 255, 255, 0.95);
            border-radius: 20px;
            box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
            overflow: hidden;
        }

        .header {
            background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 30px;
            text-align: center;
        }

        .header h1 {
            font-size: 2.5em;
            margin-bottom: 10px;
        }

        .header p {
            font-size: 1.2em;
            opacity: 0.9;
        }

        .content {
            padding: 30px;
        }

        /* 글쓰기 부스 스타일 */
        .writing-booth {
            background: linear-gradient(45deg, #ffecd2 0%, #fcb69f 100%);
            border-radius: 15px;
            padding: 25px;
            margin-bottom: 30px;
            box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
        }

        .booth-title {
            font-size: 1.5em;
            color: #8b4513;
            margin-bottom: 20px;
            text-align: center;
        }

        .input-group {
            margin-bottom: 15px;
        }

        .input-group label {
            display: block;
            margin-bottom: 8px;
            color: #8b4513;
            font-weight: bold;
        }

        .input-group input,
        .input-group textarea {
            width: 100%;
            padding: 12px;
            border: 2px solid #ddd;
            border-radius: 10px;
            font-size: 16px;
            font-family: inherit;
        }

        .input-group textarea {
            height: 100px;
            resize: vertical;
        }

        /* 이미지 업로드 부분 */
        .image-upload-section {
            background: #f0f8ff;
            border: 2px dashed #4169e1;
            border-radius: 10px;
            padding: 20px;
            text-align: center;
            margin-bottom: 15px;
        }

        .file-input-wrapper {
            position: relative;
            display: inline-block;
        }

        .file-input {
            display: none;
        }

        .file-input-btn {
            background: linear-gradient(45deg, #4169e1, #00bfff);
            color: white;
            padding: 12px 25px;
            border: none;
            border-radius: 25px;
            cursor: pointer;
            font-size: 16px;
            font-weight: bold;
            transition: transform 0.3s ease;
        }

        .file-input-btn:hover {
            transform: scale(1.05);
        }

        .image-preview {
            margin-top: 15px;
            max-width: 200px;
            max-height: 200px;
            border-radius: 10px;
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
        }

        .progress-container {
            margin-top: 15px;
            display: none;
        }

        .progress-bar {
            width: 100%;
            height: 20px;
            background-color: #e0e0e0;
            border-radius: 10px;
            overflow: hidden;
        }

        .progress-fill {
            height: 100%;
            background: linear-gradient(45deg, #4caf50, #8bc34a);
            width: 0%;
            transition: width 0.3s ease;
            border-radius: 10px;
        }

        .progress-text {
            text-align: center;
            margin-top: 5px;
            color: #666;
            font-weight: bold;
        }

        .submit-btn {
            background: linear-gradient(45deg, #ff6b6b, #ee5a24);
            color: white;
            padding: 15px 30px;
            border: none;
            border-radius: 25px;
            font-size: 18px;
            font-weight: bold;
            cursor: pointer;
            width: 100%;
            transition: transform 0.3s ease;
        }

        .submit-btn:hover {
            transform: scale(1.02);
        }

        .submit-btn:disabled {
            opacity: 0.6;
            cursor: not-allowed;
            transform: none;
        }

        /* 메시지 보드 스타일 */
        .message-board {
            background: linear-gradient(45deg, #a8edea 0%, #fed6e3 100%);
            border-radius: 15px;
            padding: 25px;
            box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
        }

        .board-title {
            font-size: 1.5em;
            color: #2c3e50;
            margin-bottom: 20px;
            text-align: center;
        }

        .message-card {
            background: white;
            border-radius: 15px;
            padding: 20px;
            margin-bottom: 20px;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
            position: relative;
            transition: transform 0.3s ease;
        }

        .message-card:hover {
            transform: translateY(-5px);
        }

        .message-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
        }

        .message-author {
            font-weight: bold;
            color: #2c3e50;
            font-size: 1.1em;
        }

        .message-time {
            color: #7f8c8d;
            font-size: 0.9em;
        }

        .message-content {
            color: #34495e;
            line-height: 1.6;
            margin-bottom: 15px;
        }

        .message-image {
            max-width: 100%;
            max-height: 300px;
            border-radius: 10px;
            margin-bottom: 15px;
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
        }

        .message-actions {
            display: flex;
            gap: 10px;
        }

        .edit-btn, .delete-btn {
            padding: 8px 16px;
            border: none;
            border-radius: 20px;
            cursor: pointer;
            font-size: 14px;
            font-weight: bold;
            transition: transform 0.3s ease;
        }

        .edit-btn {
            background: linear-gradient(45deg, #3498db, #2980b9);
            color: white;
        }

        .delete-btn {
            background: linear-gradient(45deg, #e74c3c, #c0392b);
            color: white;
        }

        .edit-btn:hover, .delete-btn:hover {
            transform: scale(1.05);
        }

        .no-messages {
            text-align: center;
            color: #7f8c8d;
            font-size: 1.2em;
            padding: 40px;
        }

        .loading {
            text-align: center;
            padding: 20px;
            color: #666;
            font-size: 1.1em;
        }

        /* 편집 모드 스타일 */
        .edit-mode {
            background: #fff3cd;
            border: 2px solid #ffc107;
        }

        .edit-input, .edit-textarea {
            width: 100%;
            padding: 10px;
            border: 2px solid #ffc107;
            border-radius: 8px;
            font-family: inherit;
            margin-bottom: 10px;
        }

        .save-btn, .cancel-btn {
            padding: 8px 16px;
            border: none;
            border-radius: 20px;
            cursor: pointer;
            font-size: 14px;
            font-weight: bold;
            margin-right: 10px;
        }

        .save-btn {
            background: linear-gradient(45deg, #28a745, #20c997);
            color: white;
        }

        .cancel-btn {
            background: linear-gradient(45deg, #6c757d, #5a6268);
            color: white;
        }
    </style>
</head>
<body>
    <div class="playground-container">
        <div class="header">
            <h1>🎠 놀이공원 메시지 게시판</h1>
            <p>📸 이미지와 함께 즐거운 추억을 공유해보세요!</p>
        </div>

        <div class="content">
            <!-- 글쓰기 부스 -->
            <div class="writing-booth">
                <h2 class="booth-title">✏️ 글쓰기 부스</h2>
                
                <div class="input-group">
                    <label for="authorInput">작성자 이름:</label>
                    <input type="text" id="authorInput" placeholder="이름을 입력하세요">
                </div>

                <div class="input-group">
                    <label for="messageInput">메시지:</label>
                    <textarea id="messageInput" placeholder="즐거운 메시지를 작성해보세요!"></textarea>
                </div>

                <div class="input-group">
                    <label>이미지 첨부:</label>
                    <div class="image-upload-section">
                        <div class="file-input-wrapper">
                            <input type="file" id="imageInput" class="file-input" accept="image/*">
                            <button class="file-input-btn" onclick="document.getElementById('imageInput').click()">
                                📷 이미지 선택하기
                            </button>
                        </div>
                        <div id="imagePreview"></div>
                        <div class="progress-container" id="progressContainer">
                            <div class="progress-bar">
                                <div class="progress-fill" id="progressFill"></div>
                            </div>
                            <div class="progress-text" id="progressText">업로드 중...</div>
                        </div>
                    </div>
                </div>

                <button class="submit-btn" id="submitBtn" onclick="submitMessage()">🎪 메시지 게시하기</button>
            </div>

            <!-- 메시지 보드 -->
            <div class="message-board">
                <h2 class="board-title">📋 메시지 보드</h2>
                <div id="messagesList">
                    <div class="loading">메시지를 불러오는 중...</div>
                </div>
            </div>
        </div>
    </div>

    <script>
        // ⭐ 1단계: Firebase 설정하기
        // 여러분의 Firebase 프로젝트 설정을 여기에 입력하세요!
        const firebaseConfig = {
            // 여기에 여러분의 Firebase 설정을 넣어주세요
            // apiKey, authDomain, projectId, storageBucket 등...
        };

        // Firebase 초기화하기
        firebase.initializeApp(firebaseConfig);
        const db = firebase.firestore();
        const storage = firebase.storage();

        // 전역 변수들
        let messages = [];
        let editingMessageId = null;
        let selectedFile = null;

        // ⭐ 2단계: 이미지 파일 선택했을 때 실행되는 함수
        document.getElementById('imageInput').addEventListener('change', function(e) {
            const file = e.target.files[0];  // 선택한 파일 가져오기
            
            if (file) {
                selectedFile = file;  // 선택한 파일을 저장해둠
                
                // 미리보기 이미지 만들기 (마법같은 FileReader 사용!)
                const reader = new FileReader();
                reader.onload = function(e) {
                    const preview = document.getElementById('imagePreview');
                    preview.innerHTML = `
                        <p>선택한 이미지:</p>
                        <img src="${e.target.result}" class="image-preview" alt="미리보기">
                    `;
                };
                reader.readAsDataURL(file);  // 파일을 읽어서 이미지로 보여줌
                
                console.log('이미지가 선택되었어요:', file.name);
            }
        });

        // ⭐ 3단계: 이미지를 Firebase Storage에 업로드하는 함수
        async function uploadImage(file) {
            return new Promise((resolve, reject) => {
                // 파일 이름을 특별하게 만들기 (현재시간 + 원래이름)
                const fileName = Date.now() + '_' + file.name;
                
                // Firebase Storage의 'images' 폴더에 저장
                const storageRef = storage.ref('images/' + fileName);
                const uploadTask = storageRef.put(file);

                // 진행상황 표시하기
                const progressContainer = document.getElementById('progressContainer');
                const progressFill = document.getElementById('progressFill');
                const progressText = document.getElementById('progressText');
                
                progressContainer.style.display = 'block';  // 진행바 보이기

                // 업로드 진행상황 감시하기
                uploadTask.on('state_changed', 
                    function(snapshot) {
                        // 진행률 계산하기 (0~100%)
                        const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
                        
                        // 진행바 업데이트
                        progressFill.style.width = progress + '%';
                        progressText.textContent = `업로드 중... ${Math.round(progress)}%`;
                        
                        console.log('업로드 진행률:', progress + '%');
                    },
                    function(error) {
                        // 에러가 발생했을 때
                        console.error('업로드 에러:', error);
                        progressText.textContent = '업로드 실패 😢';
                        reject(error);
                    },
                    function() {
                        // 업로드 완료!
                        progressText.textContent = '업로드 완료! 🎉';
                        
                        // 다운로드 URL 가져오기
                        uploadTask.snapshot.ref.getDownloadURL().then(function(downloadURL) {
                            console.log('이미지 업로드 완료! URL:', downloadURL);
                            
                            // 진행바 숨기기
                            setTimeout(() => {
                                progressContainer.style.display = 'none';
                                progressFill.style.width = '0%';
                            }, 1000);
                            
                            resolve(downloadURL);  // URL을 반환
                        });
                    }
                );
            });
        }

        // ⭐ 4단계: 메시지 제출하는 함수 (이미지 업로드 포함)
        async function submitMessage() {
            const author = document.getElementById('authorInput').value.trim();
            const message = document.getElementById('messageInput').value.trim();
            const submitBtn = document.getElementById('submitBtn');

            // 빈칸 체크하기
            if (!author || !message) {
                alert('작성자와 메시지를 모두 입력해주세요! 😊');
                return;
            }

            // 버튼 비활성화 (중복 클릭 방지)
            submitBtn.disabled = true;
            submitBtn.textContent = '게시 중...';

            try {
                let imageUrl = null;
                
                // 이미지가 선택되었다면 업로드하기
                if (selectedFile) {
                    console.log('이미지 업로드 시작!');
                    imageUrl = await uploadImage(selectedFile);
                }

                // Firestore에 메시지 저장하기
                const messageData = {
                    author: author,
                    message: message,
                    timestamp: firebase.firestore.FieldValue.serverTimestamp(),
                    imageUrl: imageUrl  // 이미지 URL도 함께 저장
                };

                await db.collection('messages').add(messageData);

                // 입력창 비우기
                document.getElementById('authorInput').value = '';
                document.getElementById('messageInput').value = '';
                document.getElementById('imageInput').value = '';
                document.getElementById('imagePreview').innerHTML = '';
                selectedFile = null;

                console.log('메시지가 성공적으로 저장되었어요!');
                
                // 성공 메시지
                alert('메시지가 게시되었어요! 🎉');

            } catch (error) {
                console.error('메시지 저장 중 에러:', error);
                alert('메시지 게시에 실패했어요 😢');
            }

            // 버튼 다시 활성화
            submitBtn.disabled = false;
            submitBtn.textContent = '🎪 메시지 게시하기';
        }

        // ⭐ 5단계: 메시지 목록 표시하는 함수 (이미지 포함)
        function displayMessages() {
            const messagesList = document.getElementById('messagesList');
            
            if (messages.length === 0) {
                messagesList.innerHTML = '<div class="no-messages">아직 메시지가 없어요. 첫 번째 메시지를 작성해보세요! 🌟</div>';
                return;
            }

            let messagesHTML = '';
            
            messages.forEach(msg => {
                const time = msg.timestamp ? msg.timestamp.toDate().toLocaleString() : '방금 전';
                
                // 편집 모드인지 확인
                if (editingMessageId === msg.id) {
                    // 편집 모드 HTML
                    messagesHTML += `
                        <div class="message-card edit-mode">
                            <input type="text" class="edit-input" id="edit-author-${msg.id}" value="${msg.author}">
                            <textarea class="edit-textarea" id="edit-message-${msg.id}">${msg.message}</textarea>
                            ${msg.imageUrl ? `<img src="${msg.imageUrl}" class="message-image" alt="첨부 이미지">` : ''}
                            <div class="message-actions">
                                <button class="save-btn" onclick="saveMessage('${msg.id}')">💾 저장</button>
                                <button class="cancel-btn" onclick="cancelEdit()">❌ 취소</button>
                            </div>
                        </div>
                    `;
                } else {
                    // 일반 모드 HTML
                    messagesHTML += `
                        <div class="message-card">
                            <div class="message-header">
                                <span class="message-author">${msg.author}</span>
                                <span class="message-time">${time}</span>
                            </div>
                            <div class="message-content">${msg.message}</div>
                            ${msg.imageUrl ? `<img src="${msg.imageUrl}" class="message-image" alt="첨부 이미지" onclick="viewFullImage('${msg.imageUrl}')">` : ''}
                            <div class="message-actions">
                                <button class="edit-btn" onclick="editMessage('${msg.id}')">✏️ 수정</button>
                                <button class="delete-btn" onclick="deleteMessage('${msg.id}')">🗑️ 삭제</button>
                            </div>
                        </div>
                    `;
                }
            });
            
            messagesList.innerHTML = messagesHTML;
        }

        // ⭐ 6단계: 이미지 클릭했을 때 크게 보여주는 함수
        function viewFullImage(imageUrl) {
            // 새 창에서 이미지 보기
            window.open(imageUrl, '_blank');
        }

        // 메시지 편집 함수
        function editMessage(messageId) {
            editingMessageId = messageId;
            displayMessages();
        }

        // 편집 취소 함수
        function cancelEdit() {
            editingMessageId = null;
            displayMessages();
        }

        // 메시지 저장 함수
        async function saveMessage(messageId) {
            const newAuthor = document.getElementById(`edit-author-${messageId}`).value.trim();
            const newMessage = document.getElementById(`edit-message-${messageId}`).value.trim();

            if (!newAuthor || !newMessage) {
                alert('작성자와 메시지를 모두 입력해주세요!');
                return;
            }

            try {
                await db.collection('messages').doc(messageId).update({
                    author: newAuthor,
                    message: newMessage,
                    timestamp: firebase.firestore.FieldValue.serverTimestamp()
                });
                
                editingMessageId = null;
                console.log('메시지가 수정되었어요!');
                
            } catch (error) {
                console.error('메시지 수정 중 에러:', error);
                alert('메시지 수정에 실패했어요 😢');
            }
        }

        // 메시지 삭제 함수
        async function deleteMessage(messageId) {
            if (confirm('정말로 이 메시지를 삭제하시겠어요?')) {
                try {
                    const messageDoc = await db.collection('messages').doc(messageId).get();
                    const messageData = messageDoc.data();
                    
                    // 이미지가 있다면 Storage에서도 삭제
                    if (messageData.imageUrl) {
                        const imageRef = storage.refFromURL(messageData.imageUrl);
                        await imageRef.delete();
                        console.log('이미지도 함께 삭제했어요!');
                    }
                    
                    // Firestore에서 메시지 삭제
                    await db.collection('messages').doc(messageId).delete();
                    
                    console.log('메시지가 삭제되었어요!');
                    
                } catch (error) {
                    console.error('메시지 삭제 중 에러:', error);
                    alert('메시지 삭제에 실패했어요 😢');
                }
            }
        }

        // ⭐ 7단계: 실시간으로 메시지 목록 업데이트하기
        function setupRealtimeListener() {
            db.collection('messages')
              .orderBy('timestamp', 'desc')
              .onSnapshot((snapshot) => {
                  messages = [];
                  snapshot.forEach((doc) => {
                      messages.push({
                          id: doc.id,
                          ...doc.data()
                      });
                  });
                  displayMessages();
                  console.log('메시지 목록이 업데이트되었어요! 총', messages.length, '개');
              });
        }

        // 페이지 로드되면 실행
        window.onload = function() {
            console.log('🎪 놀이공원 메시지 게시판에 오신 걸 환영해요!');
            setupRealtimeListener();
        };
    </script>
</body>
</html>

미리 보기

반응형