PWA

ChatGPT와 함께 PWA Blog 개발 - DB CRUD

그랜파 개발자 2024. 8. 28. 15:02

my-blog Firestore Database CRUD

1. 프로젝트

1. 프로젝트 설정

vue create my-blog

? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, PWA, Router, Vuex
? Choose a version of Vue.js that you want to start the project with 2.x
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N) n

2. vuetify 설치

cd my-blog

vue add vuetify
 
? Choose a preset: (Use arrow keys)
Vuetify 2 - Configure Vue CLI (advanced)
> Vuetify 2 - Vue CLI (recommended)
Vuetify 2 - Prototype (rapid development)
Vuetify 3 - Vite (preview)
Vuetify 3 - Vue CLI (preview)

3. 프로그램 설치

npm install firebase
npm install sanitize-html
npm install uuid

4. 프로젝트 폴더 구조

2. my-blog DB CRUD

1. users

users : 사용자 collections
 
id : collection id
email : 이메일
name : 이름 (별명 포함)
subscriptions[] : 구독하는 글쓴이들 (user.id) 배열
uids[] : Google auth를 통해 돌려받는 uid, 계정연동 (uid) 배열

 

1. 등록된 전체 회원 정보를 가져오기

async fetchUsers({ commit }) {
    try {
      const usersSnapshot = await db.collection('users').get();
      const users = usersSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
    } catch (error) {
      commit('setError', error.message);
    }
},

2. 회원 등록

async register({ commit, dispatch }, { email, password, name }) {
    try {
        // 이메일과 비밀번호로 구글 계정을 만든다.
        const { user } = await auth.createUserWithEmailAndPassword(email, password, name);

        // 구글 계정에서 받은 uid를 웹앱 계정 uids에 넣고 웹앱에 계정을 만든다.
        const newUser = {
            email: email,
            name: name,
            uids: [user.uid]
        };
        // newUser 계정 정보로 웹앱에 계정을 만든다.
        dispatch('addUser', newUser);      
    } catch (error) {
        console.log(error);
        commit('setError', error.message);
    }
},

async addUser({ dispatch, commit }, user) {
    try {
        await db.collection('users').add(user)
        .then(newUser => {        
            // 로그인 설정
            user.id = newUser.id;
            console.log('addUser :', user);
            commit('setUser', user);
        });
        // 등록된 모든 회원 정보 읽기      
        dispatch('fetchUsers');
    } catch (error) {
        console.error('Error adding user:', error);
    }
 },

3. 로그인

async loginUser() {
    await this.login({ email: this.email, password: this.password });
}

async login({ commit, dispatch }, { email, password }) {
    try {
        // Google Auth로 구글에 로그인하여 구글의 계정을 받아온다.
        const { user } = await auth.signInWithEmailAndPassword(email, password);;
        // Google 계정의 uid로  웹앱의 계정 정보를 가져와 로그인 설정을 한다.
        dispatch('fetchUserWithUid', {uid: user.uid});       
        router.push("/");   // home으로
    } catch (error) {
      commit('setError', error.message);
    }
},

// Google 계정의 uid로  웹앱의 계정 정보를 가져
async fetchUserWithUid({ commit }, {uid}) {
    try {
        const querySnapshot = await db.collection('users').where('uids', 'array-contains', uid).get();
        const user = querySnapshot.docs. map(doc => ({ id: doc.id, ...doc.data() }));

        // 로그인 설정을 한다.
        commit('setUser', user[0]);
    } catch (error) {
        console.error('Error fetching user:', error);
    }            
},

4. Google 계정 연동

async addGoogleUid({ commit, dispatch, getters }) {
    try {
        // Google 계정에 로그인하여 계정 정보를 가져온다.
        const { user } = await auth.signInWithPopup(googleProvider);
        try {
            // 이미 연동된 회원의 경우 알림 메시지 출력한다.
            const myUser = getters.getUserByUid(user.uid);
            if (myUser) {
                console.log("sos:", myUser);
                // 이미 연동되어 있다.
                commit('setError','이미 연동되어 있습니다.');
            } else {
                // 구글 연동을 진행한다.
                dispatch('addUidToUser', user.uid);
            }          
        } catch (error) {
            commit('setError','Error adding google uid');
        }
    } catch (error) {
        commit('setError', error.message);
    }
},

// Google 계정의 uid를 웹앱 계정의 uids에 저장한다.
async addUidToUser({ state }, newUid) {      
    //console.log('user.id:', state.user.id);
    if (state.user) {
        try {
            await db.collection('users').doc(state.user.id).update({
                uids: firebase.firestore.FieldValue.arrayUnion(newUid)
            });
            // Update the local state if neededa
            state.user.uids = [...(state.user.uids || []), newUid];
        } catch (error) {
            console.error('Error adding UID to user:', error);
        }
    }
},

5. Google 계정으로 로그인

async googleLogin({ commit, getters }) {
    try {
        // 구글 계정에 로그인
        const { user } = await auth.signInWithPopup(googleProvider);
        try {
            // 구글계정의 uid로 웹앱 계정의 정보를 가져옴.
            // 웹앱 계정은 사이트에 접속할 때 전체 회원 정보를 로드하였으므로 
            // 이미 로드된 회원 리스트에서 구글 계정 uid를 가진 myUser를 가져온다.
            const myUser = getters.getUserByUid(user.uid);
            if (myUser) {
               commit('setUser', myUser);
               router.push("/");   // home으로
            } else {
               console.log('등록된 회원이 아닙니다.');
               // 이경우 회원 가입 페이지로 이동 필요
            }
        } catch (error) {
            console.error('Error adding user:', error);
        }
    } catch (error) {
        commit('setError', error.message);
    }
},

// 이미 읽어 둔 회원 리스트에서 회원 정보를 얻는다.
getUserByUid: (state) => (uid) => {
    return state.users.find(user => user.uids && user.uids.includes(uid));
}

2. posts

posts : 쓴글 collections
 
id : collection id
title : 제목
content : 내용
createdAt : 글쓴날
userId : 글쓴이 user.id
userName : 글쓴이 이름
viewedBy[] : 글 조회 (date, userId) 배열
views : 조회수

1. 글쓰기

async submitPost() {  
    const user = this.$store.state.auth.user;
    if (user) {
        try {         
            await db.collection('posts').add({
                title: this.title,
                content: this.content,
                userId: user.id,
                userName: user.name,
                createdAt: new Date(),
                views: 0, // Initialize view count
                viewedBy: [] // Initialize viewedBy array
            });          
            this.$router.push('/');
        } catch (error) {
            console.error("Error writing document: ", error);
        }
    } else {
        console.error("User not authenticated");
    }
},

2. 회원의 글 가져오기

posts: []  : 글쓴이의 post들
userId = 글쓴이의 user.id

try {
    const querySnapshot = await db.collection('posts').where('userId', '==', userId).get();
    this.posts = querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
} catch (error) {
    this.error = error;
} finally {
    this.loading = false;
}

3. 나의 독자들 가져오기

회원 정보 항목 중 subscriptions[] 에 나의 user.id를 가지고 있는 회원들의 리스트를 가져 옵니다.

async fetchSubscribers() {
      const userId = this.$store.state.auth.user.id;
      const querySnapshot = await db.collection('users').where('subscriptions', 'array-contains', userId).get();
      this.subscribers = querySnapshot.docs.map(doc => ({  id: doc.id,  ...doc.data() }));
    },

4. 구독하기

내 회원 정보 중 구독 리스트 subscriptions[]에 포함된 user들의 글들을 가져옵니다.

const userDoc = await db.collection('users').doc(this.user.id).get();
if (userDoc.exists) {
    // 구독 리스트
    const subscriptions = userDoc.data().subscriptions;
    if (subscriptions.length) {
        const querySnapshot = await db.collection('posts').where('userId', 'in', subscriptions).get();
        this.subscribedPosts = querySnapshot.docs.map(doc => ({ id: doc.id,  ...doc.data()  }));
    }
}

5. 글 검색하기

title 또는 content 에 검색어(searchQuery)를 포함하는 글들을 가져옵니다.

const querySnapshot = await db.collection('posts').get();
this.searchResults = querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }))
.filter(post => 
    post.title.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
    post.content.toLowerCase().includes(this.searchQuery.toLowerCase())
);

6. 글 상세 보기

  1. poisId로 글을 읽는다.
  2. 글을 화면에 나타낼 때 줄바꾸기 등의 이유로 sanitizeContent를 사용한다.
  3. 글쓴이의 회원 정보를 가져온다.
  4. 오늘 처음 읽는 회원이면 조회수 증가시킨다.
const postRef = db.collection('posts').doc(postId);
const postDoc = await postRef.get();
if (postDoc.exists) {
    this.post = postDoc.data();
    // 줄바꾸기 등 일부 html을 사용한다. 
    this.postContent = this.sanitizeContent(this.post.content);
    this.loadComments(postId);  

    // author 정보를 가져오자.
    // 이미 읽어둔 전체 회원 정보에서 글쓴이의 회원 정보를 가져온다.
    const users = this.$store.state.auth.users;
    this.author = users.find(user => user.id === this.post.userId);

    // 글의 조회수를 처리한다. 
    // 한 사람은 하루에 하나의 조회수를 증가시킬 수 있다.
    const today = new Date().toISOString().split('T')[0];

    let userId;
    const user = this.$store.state.auth.user;
    if (user) {
        // 계정 등록된 user
        userId = user.id;   
    } else {
        // 계정 등록이 되지 않은 user
        userId = this.getOrCreateAnonymousUserId();
    }
    // 조회수를 증가시키기 위해 이미 조회한 회원인지 확인한다.
    const alreadyViewed = this.post.viewedBy.some(view => view.userId === userId && view.date === today);
    if (!alreadyViewed) {          
        await postRef.update({
            views: firebase.firestore.FieldValue.increment(1),
            viewedBy: firebase.firestore.FieldValue.arrayUnion({ userId, date: today })
        });
        this.post.views += 1; // Update the local view count
    }
}

7. 댓글 쓰기

async addComment() {     
    const postId = this.$route.params.id;
    const user = this.$store.state.auth.user;
    const userName = user.name;

    // 댓글의 구조
    const comment = {
        content: this.newComment,
        userId: user.id,
        userName,
        createdAt: new Date(),
        replies: [] // 댓글에 대한 답글
    };

    try {
        await db.collection('posts').doc(postId).collection('comments').add(comment)
        .then(addedComment => {
           // 추가된 댓글의 ID를 받아 넣는다.
            comment.id = addedComment.id;
            this.comments.push(comment);
        });        
       this.newComment = '';
   } catch (error) {
      this.error = error;
   }
},

8. 댓글 삭제

async deleteComment(commentId) {
    const postId = this.$route.params.id;
    const commentRef = await db.collection('posts').doc(postId).collection('comments').doc(commentId);
    const commentDoc = await commentRef.get();
    if (commentDoc.exists) {
        const commentData = commentDoc.data();
        // 답글이 없어야 댓글을 삭제할 수 있다.
        if (commentData.replies.length === 0) {
            await commentRef.delete();
            this.comments = this.comments.filter(c => c.id !== commentId);
        } else {
            alert('Cannot delete a comment with replies.');
        }
    }
},  

9. 댓글 가져오기

async loadComments(postId) {
    try {
        const commentsSnapshot = await db.collection('posts').doc(postId).collection('comments').get();
        this.comments = commentsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
    } catch (error) {
        this.error = error;
    }
},

10. 댓글에 답글 쓰기

async addReply(commentId) {
    const postId = this.$route.params.id;
    const userId = this.$store.state.auth.user.id;
    const userName = this.$store.state.auth.user.name;

    // 답글의 구조
    const reply = {
        id: uuidv4(),   // 답글의 id 생성
        userId,
        content: this.newReply,
        userName,
        createdAt: new Date()
    };

    try {
        const commentRef = db.collection('posts').doc(postId).collection('comments').doc(commentId);
        await commentRef.update({
            replies: firebase.firestore.FieldValue.arrayUnion(reply)
        });
        const comment = this.comments.find(comment => comment.id === commentId);
        comment.replies.push(reply);
    } catch (error) {
        this.error = error;
    }
},

11. 답글 삭제

async deleteReply(commentId, reply) {
    const postId = this.$route.params.id;

    const commentRef = db.collection('posts').doc(postId).collection('comments').doc(commentId);
    const commentDoc = await commentRef.get();
    if (commentDoc.exists) {
        const commentData = commentDoc.data();
        const userId = this.$store.state.auth.user.id;
        if (reply.userId === userId) {  
            const updatedReplies = commentData.replies.filter(r => r.id !== reply.id);
            await commentRef.update({
                replies: updatedReplies
            });

            const comment = this.comments.find(c => c.id === commentId);
            if (comment) {
                comment.replies = updatedReplies;
            }
        }
    }
},