Vue PWA mylog

mylog 댓글, 답글

그랜파 개발자 2024. 11. 17. 05:13

Vue로 PWA 개발 - 그랜파 개발자

Vue 프로젝트 Beta Test : mylog, 일상의 기록

개발이 진행됨에 따라 소스 코드를 계속 추가해 갑니다.

mylog 댓글, 답글

마이로그 상세 보기에서 댓글과 댓글에 대한 답글을 쓸 수 있습니다. 마이로그는 하나의 마이로그에 대해 여러개의 댓글을 쓸 수 있고, 각 댓글에 대한 답글의 기능도 있습니다. 각 댓글에 대해 여러 개의 답글이 있다는 뜻입니다. 답글에 대한 댓글, 댓글에 대한 답글, 또 댓글… 이렇게 댓글 트리의 깊이를 더해 가는 것은 개발자 또는 요구에 따라 필요하겠지만, 마이로그는 댓글과 그것에 대한 답글까지만 기능을 구현합니다.

 

1. 댓글과 답글 로드, 저장

 

댓글은 마이로그에 종속 되므로 각 마이로그의 comments 서브 컬렉션에 저장되고, 댓글에 대한 답글은 comments의 서브 컬렉션 replies 서브 컬렉션에 저장이 됩니다.마이로그 상세보기 페이지가 열릴 때 마이로그에 대한 댓글도 함께 로드합니다. 마이로그가 페이지에 표시될 때 댓글 또한 같이 보여지는 것입니다. 댓글이 보이면 답글 또한 보이는 것이 좋겠습니다. 그러므로 댓글을 로드할 때 각 댓글에 대한 답글도 함께 로드합니다.

1-1. 댓글 로드

마이로그에 대한 댓글 로드 입니다. mylogId를 가진 댓글들입니다.

const q = query(collection(db, "mylogs", mylogId, "comments"), orderBy("createdAt", "asc"));

1-2. 답글 로드

마이로그 댓글에 대한 답글 로드 입니다. 답글을 로드하면서 각 답글의 commentId를 가진 댓글들을 로드합니다.

const q = query(collection(db, "mylogs", mylogId, "comments", commentId, "replies"), orderBy("createdAt", "asc"));

1-3. 댓글 저장

댓글의 저장입니다. mylogId에 대한 서브 컬렉션 comments에 댓글을 저장합니다.

const commentRef = await addDoc(collection(db, "mylogs", mylogId, "comments"), {
    content: content, userName: userName, userId: userId, createdAt: new Date(),
});

1-4. 답글 저장

댓글에 대한 답글의 저장입니다. mylogId를 가진 서브 컬렉션 comments에 commentId를 가진 replies 서브 컬렉션에 답글을 저장합니다.

// db에 답글 저장
const replyRef = await addDoc(collection(db, "mylogs", mylogId, "comments", commentId, "replies"), {
    content: content, userName: userName, userId: userId, createdAt: new Date()    
});

 

2. MyLogView.vue

<!-- src/views/MyLogView.vue -->
<template>
  <v-container>
    <v-card v-if="mylog">
       . . .
      <!-- 댓글 작성 폼 -->
      <v-form v-if="user" @submit.prevent="submitComment" class="ml-2 mr-2">
        <v-textarea outlined style="border-color: #fffeee;" v-model="newComment" label="댓글 쓰기 ..." rows="3" required></v-textarea> 
        <v-btn class="ml-1 mt-n10" small outlined type="submit" color="primary">댓글</v-btn>       
      </v-form>

      <v-list >
        <!-- 댓글 리스트 -->
        <v-list-item v-for="comment in comments" :key="comment.id"> 
          <v-list-item-content>
            <!-- eslint-disable -->
            <v-list-item-subtitle style="white-space: normal;" v-html="sanitizeContent(comment.content)"></v-list-item-subtitle>
            <!--댓글 -->
            <!-- eslint-enable -->
            <v-list-item-subtitle class="mt-1"> 
              {{ comment.userName }} . {{ formatDate(comment.createdAt) }}                 
              <v-btn v-if="isOwner(comment.userId)" icon @click="doDeleteComment(comment.id)">
                <v-icon>mdi-delete</v-icon>
              </v-btn>
              <!-- reply 버튼 -->  
              <v-btn v-if="user" small text @click="showReplyForm(comment.id)">답글</v-btn>
            </v-list-item-subtitle>    

            <!-- 답글 리스트 -->
            <v-list class="ml-5" v-if="comment.replies && comment.replies.length">
              <!-- 답글 -->
              <v-list-item v-for="reply in comment.replies" :key="reply.id">
                <v-list-item-content>
                  <!-- eslint-disable -->
                  <v-list-item-subtitle style="white-space: normal;" v-html="sanitizeContent(reply.content)"></v-list-item-subtitle>
                  <!-- eslint-enable -->
                  <v-list-item-subtitle class="ml-2">
                    {{ reply.userName }} : {{ formatDate(reply.createdAt) }}
                    <v-btn v-if="isOwner(reply.userId)" icon @click="doDeleteReply(comment.id, reply.id)">
                      <v-icon>mdi-delete</v-icon>
                    </v-btn>
                  </v-list-item-subtitle>
                </v-list-item-content>
              </v-list-item>
            </v-list>

            <!-- 답글 쓰기-->
            <v-form class="ml-5 mt-1 mr-2" v-if="replyingTo === comment.id" @submit.prevent="submitReply(comment.id)">
              <v-textarea  outlined style="border-color: #fffeee; width: 90%" v-model="newReply" label="답글 쓰기 ..." rows="3" required></v-textarea>
              <v-btn class="ma-0 mt-n12" small text @click="showReplyForm(null)">취소</v-btn>&nbsp;
              <v-btn class="ma-0 mt-n12" small text type="submit" color="primary">답글</v-btn>
            </v-form>
            <!-- reply end --->

          </v-list-item-content>
        </v-list-item>
      </v-list> 
    </v-card>
    . . .
  </v-container>
</template>

<script>
import { mapActions, mapGetters } from "vuex";
import sanitizeHtml from 'sanitize-html';

export default {
  data() {
    return {
      content: '',
      newComment: '',
      newReply: '',
      replyingTo: null,
      author: null, // 저자의 이름을 가져오기 위해
    };
  },
  async created() {
    . . .
    // comments를 로드한다.
    await this.fetchComments(mylogId);

  },
  computed: {
    . . .
  },
  methods: {  
    ...mapActions('mylogs', ['fetchMylog', 'resetError', 'updateViewCount','addComment', 'fetchComments','deleteComment', 
      'addReply', 'deleteReply', 'checkSubscription', 'subscribeToUser', 'unsubscribeFromUser']),
   . . .
    // 댓글 쓰기
    async submitComment() {
      //myLogId, content, autherName, author
      if (this.newComment.trim()) {
        await this.addComment({
          mylogId: this.$route.params.id,
          content: this.newComment,
          userName: this.$store.state.auth.user.username,
          userId: this.$store.state.auth.user.id,
        });
        this.newComment = ''; // 댓글 입력 필드 초기화
      }
    },
    // 댓글 삭제
    async doDeleteComment(commentId) {
      await this.deleteComment({
        mylogId: this.$route.params.id, 
        commentId: commentId
      });      
    },
    . . .
    // 답글 작성
    async submitReply(commentId) {
      if (this.newReply && this.newReply.trim()) {
        await this.addReply({
          mylogId: this.$route.params.id,
          commentId: commentId,
          content: this.newReply,
          userName: this.$store.state.auth.user.username,
          userId: this.$store.state.auth.user.id,
        });
        this.newReply = ''; // 입력 필드 초기화
        this.replyingTo = null;
      }
    },
    // 답글 삭제 실행
    async doDeleteReply(commentId, replyId) {
      const mylogId = this.$route.params.id;
      await this.deleteReply({
        mylogId: mylogId, 
        commentId: commentId,
        replyId: replyId
      }); 
    },
    . . .
};
</script>

 

3. store - mylogs.js

// src/store/modules/mylogs.js
import { v4 as uuidv4 } from 'uuid';
import { db, doc, collection, getDoc, getDocs, setDoc, addDoc,updateDoc, deleteDoc, arrayUnion, increment, where } from "@/firebase";
import { query, orderBy } from "@/firebase";

const state = {
  . . .
  comments: [],       // 댓글 목록 저장
};

const mutations = {
  . . .
  setComments(state, comments) {  // 현재 댓글
    state.comments = comments;
  },
  addComment(state, comment) {    // 댓글 추가
    state.comments.push(comment);
  },
  addReply(state, { commentId, reply }) {   // 댓글에 대한 답글 추가
    const comment = state.comments.find(c => c.id === commentId);
    if (comment) {
      if (!comment.replies) {
        comment.replies = [];
      }
      comment.replies.push(reply);
    }
  },
};

const actions = {
  . . .
  // comments 가져오기
  async fetchComments({ commit, dispatch }, mylogId) {
    try {
      const q = query(collection(db, "mylogs", mylogId, "comments"), orderBy("createdAt", "asc"));
      const querySnapshot = await getDocs(q);
      const comments = [];
      querySnapshot.forEach((doc) => {
        comments.push({ id: doc.id, ...doc.data(), replies: [] });
        // 댓글에 대한 답글 로드
        dispatch('fetchReplies', { mylogId, commentId: doc.id });
      });
      commit('setComments', comments); // 댓글을 상태에 저장
    } catch (error) {
      commit("setError", error);
      console.error("댓글 가져오기 실패:", error);
    }
  },
  // comments 추가 하기
  async addComment({ commit, dispatch }, { mylogId, content, userName, userId }) {
    try {
      // db에 댓글 저장
      const commentRef = await addDoc(collection(db, "mylogs", mylogId, "comments"), {
        content: content, userName: userName, userId: userId, createdAt: new Date(),
      });

      // 저장한 댓글을 상태에 로드된 댓글에 추가
      const newComment = { id: commentRef.id, content, userName, userId, createdAt: new Date() };
      commit('addComment', newComment); // 새 댓글을 상태에 추가

      // mylog의 댓글수 증가
      const mylogRef = doc(db, "mylogs", mylogId);     
      try {
        // 조회수 1 증가 또는 필드가 없을 때 1로 설정
        await updateDoc(mylogRef, {
          commentCount: increment(1)
        });
      } catch (error) {
        commit('setError', error);
      }

      dispatch('fetchMylogs');

    } catch (error) {
      commit("setError", error);
      console.error("댓글 추가 실패:", error);
    }
  },
  // 댓글 삭제
  async deleteComment({ commit, dispatch }, { mylogId, commentId }) {
    const commentRef = doc(db, "mylogs", mylogId, "comments", commentId);
    try {
      await deleteDoc(commentRef);
      dispatch('fetchComments', mylogId); // 댓글 다시 로그

      // mylog의 댓글수 증가
      const mylogRef = doc(db, "mylogs", mylogId);     
      try {
        // 조회수 1 증가 또는 필드가 없을 때 1로 설정
        await updateDoc(mylogRef, {
          commentCount: increment(-1)
        });
      } catch (error) {
        commit('setError', error);
      }

      dispatch('fetchMylogs');
      //this.comments = this.comments.filter(c => c.id !== commentId);
      console.log("댓글이 삭제되었습니다.");
    } catch (error) {
      commit("setError", error);
      console.error("댓글 삭제 실패:", error);
    }
  },
  // 댓글에 답글 추가
  async addReply({ commit }, { mylogId, commentId, content, userName, userId  }) {
    try {
      // db에 답글 저장
      const replyRef = await addDoc(collection(db, "mylogs", mylogId, "comments", commentId, "replies"), {
        content: content, userName: userName, userId: userId, createdAt: new Date()    
      });
      // 로드된 댓글에 답글 저장
      const newReply = { id: replyRef.id, content, userName, userId, createdAt: new Date() };
      commit('addReply', { commentId, reply: newReply });
    } catch (error) {
      commit("setError", error);
      console.error("답글 추가 실패:", error);
    }
  },
  // 답글 삭제
  async deleteReply({ commit, dispatch }, { mylogId, commentId, replyId }) {
    const replyRef = doc(db, "mylogs", mylogId, "comments", commentId, "replies", replyId);
    try {
      await deleteDoc(replyRef); 
      dispatch('fetchComments', mylogId); // 댓글 다시 로그
      console.log("답글이 삭제되었습니다.");
    } catch (error) {
      commit("setError", error);
      console.error("답글 삭제 실패:", error);
    }
  },  
  // 특정 댓글의 답글 불러오기
  async fetchReplies({ commit }, { mylogId, commentId }) {
    try {
      const q = query(collection(db, "mylogs", mylogId, "comments", commentId, "replies"), orderBy("createdAt", "asc"));
      const querySnapshot = await getDocs(q);
      const replies = [];
      querySnapshot.forEach((doc) => {
        replies.push({ id: doc.id, ...doc.data() });
      });
      replies.forEach(reply => {
        commit('addReply', { commentId, reply });
      });
    } catch (error) {
      commit("setError", error);
      console.error("답글 불러오기 실패:", error);
    }
  },
  . . .
};

const getters = {
  . . .
  comments: state => state.comments,
};

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
};

Vue PWA 프로젝트, mylog 코드

'Vue PWA mylog' 카테고리의 다른 글

mylog 쓰기  (0) 2024.11.20
mylog 구독  (0) 2024.11.19
mylog 모아보기  (0) 2024.11.16
mylog 조회수  (1) 2024.11.15
mylog 수정  (1) 2024.11.14