Vue로 PWA 개발

34. mylog 답글 구현

그랜파 개발자 2024. 10. 26. 15:11

각 댓글에 대해 답글이 있습니다. 마이로그 상세 보기에는 마이로그, 마이로그에 대한 댓글, 댓글에 대한 답글을 모두 나타냅니다. 마이로그에 대한 댓글을 쓸 수 있고, 댓글에 대한 답글을 쓸 수 있습니다. 답글은 바로 삭제가 가능합니다. 그러나 댓글의 경우 답글이 있으면 삭제할 수 없습니다. 답글이 있다면 답글이 모두 삭제되어야 댓글을 삭제할 수 있습니다. 마이로그는 수정할 수 있지만 삭제할 수는 없습니다.

1. 답글 쓰기

2. src/views/MyLogView.vue

<!-- src/views/MyLogView.vue -->
<template>
  <v-container>
    <v-row justify="center">
      <v-col cols="12" md="8">
        <v-card v-if="mylog">
          <v-card-title>{{ mylog.title }}</v-card-title>
          <v-card-subtitle>
            {{ mylog.userName }} Posted on: {{ mylog.createdAt.toDate().toLocaleString() }} ({{ mylog.views }})
          </v-card-subtitle>
          <v-card-text>
            <!-- eslint-disable -->
            <v-card-text v-html="content"></v-card-text>
            <!-- eslint-enable -->
          </v-card-text>

          <v-card-actions v-if="isAuthor">
            <!-- 게시물 수정 버튼 -->
            <v-btn @click="goToEditMylog()"> 수정 &nbsp; <v-icon>mdi-pencil</v-icon></v-btn>
          </v-card-actions>  

          <!-- 댓글 목록 -->
          <v-divider class="my-2"></v-divider>
          <h3>Comments</h3>
          <v-list>
            <v-list-item v-for="comment in comments" :key="comment.id"> 
              <v-list-item-content>
                <v-list-item-title>{{ comment.content}}</v-list-item-title>
                <v-list-item-subtitle>
                  {{ comment.userName }} : {{ formatDate(comment.createdAt) }} 
                  <v-btn v-if="isCommentOwner(comment.userId)" icon @click="doDeleteComment(comment.id)">
                    <v-icon>mdi-delete</v-icon>
                  </v-btn>
                </v-list-item-subtitle>

                <!-- reply -->        
                <v-list-item-action v-if="isAuthenticated">
                  <v-btn small outlined @click="showReplyForm(comment.id)"> 답글 쓰기 </v-btn>
                </v-list-item-action>
                <!-- reply list -->
                <v-list class="ml-6" v-if="comment.replies && comment.replies.length">
                  <v-list-item v-for="reply in comment.replies" :key="reply.id">
                    <v-list-item-content>
                      <v-list-item-title>{{ reply.content }}</v-list-item-title>
                      <v-list-item-subtitle>
                        {{ 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 v-if="replyingTo === comment.id" @submit.prevent="submitReply(comment.id)">
                  <v-textarea v-model="newReply" label="Write a reply..." rows="3" required></v-textarea>
                  <v-btn small text @click="showReplyForm(null)"> 취소 </v-btn>
                  <v-btn small text type="submit" color="primary" class="ml-2"> 답글 </v-btn>
                </v-form>
                <!-- reply end --->
              </v-list-item-content>
            </v-list-item>
          </v-list>

          <!-- 댓글 작성 폼 -->
          <v-form v-if="isAuthenticated" @submit.prevent="submitComment">
            <v-textarea v-model="newComment" label="Write a comment..." rows="3" required></v-textarea>
            <v-btn small outlined type="submit" color="primary"> 댓글 쓰기 </v-btn>
          </v-form>

        </v-card>
        <v-alert v-else type="error" dismissible @input="resetErrorMsg" class="my-alert">
          Mylog not found. Please check the link and try again.
        </v-alert>
        <!-- <v-alert v-if="error" type="error" dismissible @input="resetErrorMsg" class="my-alert">{{ error }}</v-alert> -->
      </v-col>
    </v-row>
  </v-container>
</template>

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

export default {
  data() {
    return {
      content: '',
      newComment: '',
      newReply: '',
      replyingTo: null,
    };
  },
  async created() {
    // 마이로그 가죠오기
    const mylogId = this.$route.params.id; // Get the article ID from the route parameters
    await this.fetchMylog(mylogId); // 마이로그 아이디로 마이로그를 가져온다.
    this.content = this.sanitizeContent(this.mylog.content);  // 내용에 포한된 html을 안전한 html로 변경한다.

    // 조회수를 증가한다.
    // 같은 회원은 몇번을 방문해도 하루 1회, 비회원도 조회수 증가시킴
    const userId = this.$store.state.auth.user ? this.$store.state.auth.user.id : null; // 로그인 여부 확인  
    await this.updateViewCount(mylogId, userId); // 조회수 업데이트 및 기록

    // comments를 로드한다.
    await this.fetchComments(mylogId); // 조회수 업데이트 및 기록

    //console.log(this.comments);
  },  
  computed: {
    ...mapGetters('mylogs',['mylog', 'comments']),   // 'mylog' mylogs store의 mylog getter로서 현재 선택된 마이로그를 돌려준다. 
    // 작성자와 로그인한 사용자 비교
    isAuthor() {
      return this.$store.state.auth.user && this.$store.state.auth.user.id === this.mylog.userId;
    },
    isAuthenticated() {
      //return !!auth.currentUser;
      return this.$store.state.auth.user;
    }
  },
  methods: { 
    // 'fetchMylog' mylogs의 action 함수로 마이로그를 firestore 에서 읽어온다. 
    ...mapActions('mylogs', ['fetchMylog', 'resetError', 'updateViewCount','addComment', 'fetchComments','deleteComment', 'addReply', 'deleteReply']),

    // content를 안전한 html로 바꿔준다.
    sanitizeContent(content) {
      return sanitizeHtml(content.replace(/\n/g, '<br>'), {
        allowedTags: ['b', 'i', 'em', 'strong', 'p', 'br'],
        allowedAttributes: {}
      });
    },
    goToEditMylog() {
      this.$router.push({ name: 'EditMyLogView', params: { mylogId: this.$route.params.id, mylog: this.mylog } });
    },
    resetErrorMsg() {
      this.resetError();
    },
    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
      });      
    },
    formatDate(date) {
      //console.log(date.toDate().toLocaleString());
      if (date && date.toDate) {
        return date.toDate().toLocaleString();
      }
      return '';
    },
    isCommentOwner(userId) {
      //console.log(userId, this.$store.state.auth.user.id);
      //console.log(userId === this.$store.state.auth.user.id);
      return userId === this.$store.state.auth.user.id;
    },
    showReplyForm(commentId) {
      this.replyingTo = commentId;
    },
    isOwner(userId) {
      return userId === this.$store.state.auth.user.id;
    },

    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
      });
      //this.$router.push({ name: "MyLogView", params: { id: mylogId } });  
    },
  }
};
</script>

3. src/store/modules/mylogs.js

Copy// src/store/modules/mylogs.js

import router from '@/router';  // Vue Router import
import { v4 as uuidv4 } from 'uuid';
import { db, doc, collection, getDoc, getDocs, addDoc, setDoc, deleteDoc, query, orderBy, updateDoc, arrayUnion, increment, where  } from "@/firebase";

const state = {
  mylog: null,
  mylogs: [],
  userMylogs: [], // 본인이 작성한 글 저장
  comments: [], // 댓글 목록 저장
  loading: false,
  error: null
};

const mutations = {
  setLoading(state, loading) {
    state.loading = loading;
  },
  setMylogs(state, mylogs) {
    state.mylogs = mylogs;
  },
  setMylog(state, mylog) {
    state.mylog = mylog;
  },
  setUserMylogs(state, mylogs) {
    state.userMylogs = mylogs;
  },
  setError(state, error) {
    state.error = error;
  },
  updateMylog(state, updatedMylog) {
    const index = state.mylogs.findIndex(mylog => mylog.id === updatedMylog.id);
    if (index !== -1) {
      state.mylogs.splice(index, 1, updatedMylog); // 상태에서 게시물 수정
    }
  },
  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 = {
  async fetchMylogs({ commit }) {
    commit('setLoading', true);
    try {
      const mylogs = [];
      //const mylogsRef = collection(db, "mylogs");
      //const querySnapshot = await getDocs(mylogsRef);
      const mylogsRef = query(collection(db, "mylogs"), orderBy("createdAt", "desc"));
      const querySnapshot = await getDocs(mylogsRef);
      querySnapshot.forEach((doc) => {
        // doc.data() is never undefined for query doc snapshots
        mylogs.push({ id: doc.id, ...doc.data() });
      });
      commit('setMylogs', mylogs);
    } catch (error) {
      console.log("error: ", error);
      commit('setError', error);
    } finally {
      commit('setLoading', false);
    }
  },
  async saveMylog({ commit, dispatch }, { title, content, userId, userName, createdAt }) {
    try {
      // 작성한 마이로그를 firestore에 저장한다.
      const mylog = await addDoc(collection(db, "mylogs"), {
        title: title,
        content: content,
        userId: userId,
        userName: userName,
        createdAt: createdAt
      });

      dispatch('fetchMylogs');  // 새 마이로그을 저장한 후 전체 마이로그를 새로 읽는다.
      commit("setError", null);

      router.push("/");   // home으로      

    } catch (error) {
      commit("setError", error.message);
    }
  },
  async fetchMylog({ commit }, mylogId) {
    //console.log('mylogId:', mylogId);
    commit('setLoading', true);
    try {
      const docRef = doc(db, "mylogs", mylogId); // Reference to the specific article document
      const docSnap = await getDoc(docRef); // Fetch the document snapshot

      if (docSnap.exists()) {
        const mylog = docSnap.data(); // Set article data
        commit('setMylog', mylog);
      } else {
        console.error("mylog가 없습니다.");
      }
    } catch (error) {
      commit('setError', error);
    } finally {
      commit('setLoading', false);
    }
  },

  // 조회 기록 업데이트 함수
  async updateViewCount({ commit, dispatch }, mylogId, userId = null) {

    let viewId;
    let viewedToday;

    // 비회원일 경우 로컬 스토리지 또는 쿠키를 사용하여 고유 ID 생성
    if (!userId) {
      viewId = localStorage.getItem('anonymousUserId');
      if (!viewId) {
        viewId = uuidv4();
        localStorage.setItem('anonymousUserId', viewId);
      }
      // dispatch('getOrCreateAnonymousUserId').then(result => {
      //   viewId = result;
      // })
    } else {
      viewId = userId; // 회원일 경우 사용자 ID
    }    

    try {     
      const today = new Date().toISOString().split('T')[0];
      const viewDocRef = doc(db, 'views', mylogId, 'users', viewId);

      // Firestore에서 해당 사용자의 조회 기록을 가져옵니다.
      const viewDoc = await getDoc(viewDocRef);      
      if (viewDoc.exists()) {
        // 오늘 조회 기록이 있는지 확인
        const lastViewed = viewDoc.data().lastViewed;
        //const today = new Date().toDateString();
        //const viewedToday = lastViewed.some(view => view.toDate().toDateString() === today);
        viewedToday = lastViewed.some(view => view === today);
      } 
      // 조회한 내역이 없으면 조회수 추가
      if (!viewedToday) {
        // Firestore의 lastViewed 배열에 조회 시간을 추가
        await setDoc(viewDocRef, {
          lastViewed: arrayUnion(today) // 배열에 서버 시간을 추가
        }, { merge: true });

        // mylog 조회수 증가
        const mylogRef = doc(db, "mylogs", mylogId);     
        try {
          // 조회수 1 증가 또는 필드가 없을 때 1로 설정
          await updateDoc(mylogRef, {
            views: increment(1)
          });
        } catch (error) {
          commit('setError', error);
        }
      }
    } catch (error) {
      console.log('views : ', error);
      commit('setError', error);
    }
  },
  // 현재 사용자가 작성한 게시물 불러오기
  async fetchUserMylogs({ commit }, userId) { 
    if (userId) { 
      try {
        const q = query(collection(db, "mylogs"), where("userId", "==", userId));
        const querySnapshot = await getDocs(q);
        const mylogs = [];
        querySnapshot.forEach((doc) => {
          mylogs.push({ id: doc.id, ...doc.data() });
        });
        commit('setUserMylogs', mylogs); // 상태에 저장
      } catch (error) {
        console.error("본인 글 가져오기 실패:", error);
        commit('setError', error);  
      }
    }
  },
  // 게시물 수정 액션
  async updateMylog({ commit }, { mylogId, updatedData }) {
    try {
      const mylogRef = doc(db, "mylogs", mylogId);
      await updateDoc(mylogRef, {
        title: updatedData.title,
        content: updatedData.content,
        updatedAt: new Date(), // 수정한 시간 기록
      });
      commit('updateMylog', { id: mylogId, ...updatedData });
      //console.log("게시물이 수정되었습니다.");
    } catch (error) {
      console.error("게시물 수정 실패:", error);
      commit('setError', error); 
    }
  },
  // 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 }, { mylogId, content, userName, userId }) {
    try {
      const commentRef = await addDoc(collection(db, "mylogs", mylogId, "comments"), {
        content: content,
        userName: userName,
        userId: userId,
        //createdAt: serverTimestamp(),
        createdAt: new Date(),
      });
      const newComment = { id: commentRef.id, content, userName, userId, createdAt: new Date() };
      commit('addComment', newComment); // 새 댓글을 상태에 추가
    } 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); // 댓글 다시 로그
      //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 {
      const replyRef = await addDoc(collection(db, "mylogs", mylogId, "comments", commentId, "replies"), {
        content: content,        
        userName: userName,
        userId: userId,
        createdAt: new Date(),
        //author: author,
        //createdAt: serverTimestamp(),        
      });
      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('fetchReplies', {mylogId, commentId}); // 댓글 다시 로그
      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 = {
  mylog: state => state.mylog,
  mylogs: state => state.mylogs,
  userMylogs: state => state.userMylogs,
  comments: state => state.comments,
  error: state => state.error,
  loading: state => state.loading
};

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

'Vue로 PWA 개발' 카테고리의 다른 글

36. mylog 찾기 구현  (0) 2024.10.28
35. mylog 찾기  (0) 2024.10.28
33. mylog 답글  (0) 2024.10.26
32. mylog 댓글 쓰기 구현  (0) 2024.10.26
31. mylog 댓글 쓰기  (0) 2024.10.26