PWA

ChatGPT와 함께 PWA Blog 개발 - 댓글 삭제

그랜파 개발자 2024. 8. 24. 05:13

61. 댓글 삭제

ChatGPT에게 물었습니다.

Please create a function to delete comments if there are no replies.

답글이 없는 경우 댓글을 삭제할 수 있는 기능을 만들어 줘.

ChatGPT

 

To implement a function that deletes comments if there are no replies, you need to add some logic to check the replies array of a comment before deleting it from Firestore. Here’s how you can do it:
답글이 없을 때 댓글을 삭제하는 기능을 구현하려면 Firestore에서 댓글을 삭제하기 전에 댓글의 replies 배열을 확인하는 로직을 추가해야 합니다. 방법은 다음과 같습니다.

  1. Update Comment Component: Add a button to delete the comment.
    댓글 구성 요소 업데이트: 댓글을 삭제할 수 있는 버튼을 추가합니다.
  2. Add Delete Functionality: Implement the logic to delete a comment if it has no replies.
    삭제 기능 추가: 댓글에 답글이 없을 경우 삭제하는 로직을 구현합니다.

Step-by-Step Implementation

단계별 구현

1. Update Comment Component

1. Comment 구성 요소 업데이트

Add Delete Button: Update the Comment.vue component to include a delete button that triggers the delete action.
삭제 버튼 추가: 삭제 작업을 트리거하는 삭제 버튼을 포함하도록 Comment.vue 구성 요소를 업데이트합니다.

<!-- src/components/Comment.vue -->
<template>
  <v-card class="mb-4">
    <v-card-title>
      {{ comment.userName }}
      <v-spacer></v-spacer>
      <v-btn icon @click="deleteComment">
        <v-icon>mdi-delete</v-icon>
      </v-btn>
    </v-card-title>
    <v-card-text>
      {{ comment.text }}
    </v-card-text>
    <v-card-actions>
      <v-btn small @click="showReplyBox = !showReplyBox">Reply</v-btn>
    </v-card-actions>
    <v-expand-transition>
      <v-text-field v-if="showReplyBox" v-model="replyText" label="Reply" @keyup.enter="postReply"></v-text-field>
    </v-expand-transition>
    <v-list v-if="comment.replies">
      <v-list-item v-for="reply in comment.replies" :key="reply.id">
        <v-list-item-content>
          <v-list-item-title>{{ reply.userName }}</v-list-item-title>
          <v-list-item-subtitle>{{ reply.text }}</v-list-item-subtitle>
        </v-list-item-content>
      </v-list-item>
    </v-list>
  </v-card>
</template>

<script>
export default {
  props: {
    comment: Object
  },
  data() {
    return {
      showReplyBox: false,
      replyText: ''
    };
  },
  methods: {
    async postReply() {
      if (!this.replyText.trim()) return;

      const newReply = {
        text: this.replyText,
        userName: 'Current User', // Replace with the actual user's name
        createdAt: new Date()
      };

      await this.$emit('add-reply', this.comment.id, newReply);

      this.replyText = '';
      this.showReplyBox = false;
    },
    async deleteComment() {
      await this.$emit('delete-comment', this.comment.id);
    }
  }
};
</script>

2. Add Delete Functionality

2. 삭제 기능 추가

Handle Delete Action in Comments List: Update the CommentsList.vue component to handle the delete action.
댓글 목록에서 삭제 작업 처리: 삭제 작업을 처리하려면 CommentsList.vue 구성 요소를 업데이트하세요.

<!-- src/components/CommentsList.vue -->
<template>
  <div>
    <v-text-field v-model="newCommentText" label="Add a comment" @keyup.enter="postComment"></v-text-field>
    <v-btn @click="postComment">Post Comment</v-btn>
    <v-list>
      <Comment v-for="comment in comments" :key="comment.id" :comment="comment" @add-reply="addReply" @delete-comment="deleteComment"></Comment>
    </v-list>
  </div>
</template>

<script>
import { db } from '../firebase';
import Comment from './Comment.vue';

export default {
  components: {
    Comment
  },
  data() {
    return {
      comments: [],
      newCommentText: ''
    };
  },
  async created() {
    const snapshot = await db.collection('comments').orderBy('createdAt').get();
    this.comments = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
  },
  methods: {
    async postComment() {
      if (!this.newCommentText.trim()) return;

      const newComment = {
        text: this.newCommentText,
        userName: 'Current User', // Replace with the actual user's name
        createdAt: new Date(),
        replies: []
      };

      const docRef = await db.collection('comments').add(newComment);
      this.comments.push({ id: docRef.id, ...newComment });
      this.newCommentText = '';
    },
    async addReply(commentId, reply) {
      const commentRef = db.collection('comments').doc(commentId);
      await commentRef.update({
        replies: firebase.firestore.FieldValue.arrayUnion(reply)
      });

      const comment = this.comments.find(c => c.id === commentId);
      if (comment) {
        comment.replies.push(reply);
      }
    },
    async deleteComment(commentId) {
      const commentRef = db.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.');
        }
      }
    }
  }
};
</script>

Summary 요약

  • Comment.vue: A component to display individual comments, add replies, and delete the comment.
    Comment.vue: 개별 댓글 표시, 댓글 추가, 댓글 삭제를 위한 컴포넌트입니다.
  • CommentsList.vue: A component to handle a list of comments, adding new comments, adding replies to comments, and deleting comments if they have no replies.
    CommentsList.vue: 댓글 목록을 처리하고, 새 댓글을 추가하고, 댓글에 대한 답글을 추가하고, 답글이 없는 경우 댓글을 삭제하는 구성 요소입니다.
  • Firestore Logic: Added functions to handle CRUD operations for comments and replies in Firestore.
    Firestore 로직: Firestore의 댓글 및 답글에 대한 CRUD 작업을 처리하는 기능을 추가했습니다.

This setup ensures that comments can be deleted only if they have no replies, providing a clear user interface for managing comments and their replies.
이 설정을 사용하면 댓글과 댓글을 관리하기 위한 명확한 사용자 인터페이스를 제공하여 댓글이 없는 경우에만 댓글을 삭제할 수 있습니다.

ChatGPT에게 물었습니다.

Please allow only commenters to delete comments.

댓글 작성자만 삭제할 수 있도록 해줘.

ChatGPT

 

To ensure that only the user who posted a comment can delete it, you need to implement authentication and authorization checks. Specifically, you need to check the currently logged-in user's ID against the ID of the user who posted the comment. 댓글을 게시한 사용자만 댓글을 삭제할 수 있도록 하려면 인증 및 승인 확인을 구현해야 합니다. 구체적으로 현재 로그인한 사용자의 ID와 댓글을 게시한 사용자의 ID를 확인해야 합니다.

Here's how you can achieve this in a Vue application using Firebase Authentication and Firestore:
Firebase 인증 및 Firestore를 사용하여 Vue 애플리케이션에서 이를 달성하는 방법은 다음과 같습니다.

Step-by-Step Implementation

단계별 구현

  1. Update the Comment Component: Add a check to display the delete button only if the current user is the commenter.
    댓글 구성 요소 업데이트: 현재 사용자가 댓글 작성자인 경우에만 삭제 버튼을 표시하려면 확인 표시를 추가하세요.
  2. Modify the Firestore Structure: Ensure that each comment stores the ID of the user who posted it.
    Firestore 구조 수정: 각 댓글이 게시한 사용자의 ID를 저장하는지 확인하세요.
  3. Handle User Authentication: Integrate Firebase Authentication to get the current user's ID.
    사용자 인증 처리: Firebase 인증을 통합하여 현재 사용자의 ID를 가져옵니다.

Step 1: Update the Comment Component

1단계: 댓글 구성요소 업데이트

Update the Comment.vue component to show the delete button only if the current user is the commenter.
현재 사용자가 댓글 작성자인 경우에만 삭제 버튼을 표시하도록 Comment.vue 구성 요소를 업데이트하세요.

<!-- src/components/Comment.vue -->
<template>
  <v-card class="mb-4">
    <v-card-title>
      {{ comment.userName }}
      <v-spacer></v-spacer>
      <v-btn v-if="isCommentOwner" icon @click="deleteComment">
        <v-icon>mdi-delete</v-icon>
      </v-btn>
    </v-card-title>
    <v-card-text>
      {{ comment.text }}
    </v-card-text>
    <v-card-actions>
      <v-btn small @click="showReplyBox = !showReplyBox">Reply</v-btn>
    </v-card-actions>
    <v-expand-transition>
      <v-text-field v-if="showReplyBox" v-model="replyText" label="Reply" @keyup.enter="postReply"></v-text-field>
    </v-expand-transition>
    <v-list v-if="comment.replies.length">
      <v-list-item v-for="reply in comment.replies" :key="reply.id">
        <v-list-item-content>
          <v-list-item-title>{{ reply.userName }}</v-list-item-title>
          <v-list-item-subtitle>{{ reply.text }}</v-list-item-subtitle>
        </v-list-item-content>
        <v-list-item-action>
          <v-btn v-if="isReplyOwner(reply)" icon @click="deleteReply(reply)">
            <v-icon>mdi-delete</v-icon>
          </v-btn>
        </v-list-item-action>
      </v-list-item>
    </v-list>
  </v-card>
</template>

<script>
export default {
  props: {
    comment: Object,
    currentUser: Object
  },
  data() {
    return {
      showReplyBox: false,
      replyText: ''
    };
  },
  computed: {
    isCommentOwner() {
      return this.comment.userId === this.currentUser.uid;
    },
    isReplyOwner() {
      return (reply) => reply.userId === this.currentUser.uid;
    }
  },
  methods: {
    async postReply() {
      if (!this.replyText.trim()) return;

      const newReply = {
        text: this.replyText,
        userName: this.currentUser.displayName, // Replace with the actual user's name
        userId: this.currentUser.uid, // Add the user's ID
        createdAt: new Date()
      };

      await this.$emit('add-reply', this.comment.id, newReply);

      this.replyText = '';
      this.showReplyBox = false;
    },
    async deleteComment() {
      await this.$emit('delete-comment', this.comment.id);
    },
    async deleteReply(reply) {
      await this.$emit('delete-reply', this.comment.id, reply);
    }
  }
};
</script>

Step 2: Modify the Firestore Structure

2단계: Firestore 구조 수정

Ensure that each comment and reply stores the ID of the user who posted it.
각 댓글과 답글이 이를 게시한 사용자의 ID를 저장하는지 확인하세요.

// src/components/CommentsList.vue
<template>
  <div>
    <v-text-field v-model="newCommentText" label="Add a comment" @keyup.enter="postComment"></v-text-field>
    <v-btn @click="postComment">Post Comment</v-btn>
    <v-list>
      <Comment v-for="comment in comments" :key="comment.id" :comment="comment" :current-user="currentUser" @add-reply="addReply" @delete-comment="deleteComment" @delete-reply="deleteReply"></Comment>
    </v-list>
  </div>
</template>

<script>
import { db, auth } from '../firebase';
import Comment from './Comment.vue';

export default {
  components: {
    Comment
  },
  data() {
    return {
      comments: [],
      newCommentText: '',
      currentUser: null
    };
  },
  async created() {
    auth.onAuthStateChanged(user => {
      if (user) {
        this.currentUser = user;
      } else {
        this.currentUser = null;
      }
    });

    const snapshot = await db.collection('comments').orderBy('createdAt').get();
    this.comments = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
  },
  methods: {
    async postComment() {
      if (!this.newCommentText.trim()) return;

      const newComment = {
        text: this.newCommentText,
        userName: this.currentUser.displayName, // Replace with the actual user's name
        userId: this.currentUser.uid, // Add the user's ID
        createdAt: new Date(),
        replies: []
      };

      const docRef = await db.collection('comments').add(newComment);
      this.comments.push({ id: docRef.id, ...newComment });
      this.newCommentText = '';
    },
    async addReply(commentId, reply) {
      const commentRef = db.collection('comments').doc(commentId);
      await commentRef.update({
        replies: firebase.firestore.FieldValue.arrayUnion(reply)
      });

      const comment = this.comments.find(c => c.id === commentId);
      if (comment) {
        comment.replies.push(reply);
      }
    },
    async deleteComment(commentId) {
      const commentRef = db.collection('comments').doc(commentId);
      const commentDoc = await commentRef.get();
      if (commentDoc.exists) {
        const commentData = commentDoc.data();
        if (commentData.userId === this.currentUser.uid && commentData.replies.length === 0) {
          await commentRef.delete();
          this.comments = this.comments.filter(c => c.id !== commentId);
        } else {
          alert('Cannot delete a comment with replies or you are not the owner.');
        }
      }
    },
    async deleteReply(commentId, reply) {
      const commentRef = db.collection('comments').doc(commentId);
      const commentDoc = await commentRef.get();
      if (commentDoc.exists) {
        const commentData = commentDoc.data();
        if (reply.userId === this.currentUser.uid) {
          const updatedReplies = commentData.replies.filter(r => r.createdAt !== reply.createdAt);
          await commentRef.update({
            replies: updatedReplies
          });

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

Summary 요약

This setup ensures that only the user who posted a comment or reply can delete it. The key parts are:
이렇게 설정하면 댓글이나 답글을 게시한 사용자만 삭제할 수 있습니다. 주요 부분은 다음과 같습니다.

  • Comment.vue: A component to display individual comments, add replies, and delete replies only if the current user is the owner.
    Comment.vue: 현재 사용자가 소유자인 경우에만 개별 댓글을 표시하고, 답글을 추가하고, 답글을 삭제하는 구성 요소입니다.
  • CommentsList.vue: A component to handle a list of comments, adding new comments, adding replies to comments, and deleting replies from comments.
    CommentsList.vue: 댓글 목록을 처리하고, 새 댓글을 추가하고, 댓글에 답글을 추가하고, 댓글에서 답글을 삭제하는 구성 요소입니다.
  • Firestore Logic: Ensure each comment and reply stores the ID of the user who posted it and perform authorization checks before deletion.
    Firestore 로직: 각 댓글과 답글이 게시한 사용자의 ID를 저장하는지 확인하고 삭제하기 전에 인증 확인을 수행합니다.

By implementing this approach, you can ensure that only the original poster of a comment or reply can delete their own contributions, providing a secure and user-friendly commenting system. 이 접근 방식을 구현하면 댓글이나 답글의 원래 게시자만 자신의 기여를 삭제할 수 있도록 보장하여 안전하고 사용자 친화적인 댓글 시스템을 제공할 수 있습니다.

ChatGPT 대답의 맥락이 달라졌습니다. ChatGPT의 대화 Context가 달라진 것 같습니다. ChatGPT가 제시한 코드에서 필요한 부분을 추출하여 적용하고, 필요에 따라 디버깅하여 수정한 후 소스 코드를 공개합니다.

ChatGPT가 시키는 대로 했습니다.

1. PostDetailView.vue 업데이트

2. 실행

3. Source Code

src/views/PostDetailView.vue

Copy<!-- src/views/PostDetailView.vue -->
<template>
  <v-container>    
    <v-progress-circular v-if="loading" indeterminate></v-progress-circular>

    <v-card v-if="!loading && post">

      <v-card-title>{{ post.title }}</v-card-title>
      <v-card-text>{{ post.userName }} - {{ formatDate(post.createdAt) }} &nbsp;&nbsp; Views: {{ post.views }}</v-card-text>
      <v-card-text v-html="postContent"></v-card-text>

      <!-- Comments Section -->
      <v-divider class="my-4"></v-divider>
      <h3>Comments</h3>
      <v-list>

        <v-list-item v-for="comment in comments" :key="comment.id">

          <v-list-item-content>
            <!-- comments -->
            <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="deleteComment(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="isReplyOwner(reply.userId)" icon @click="deleteReply(comment.id, reply)">
                      <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="addReply(comment.id)">
              <v-text-field v-model="newReply" label="Add a reply" required></v-text-field>
              <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>

      <!-- Add Comment Form -->
      <v-form v-if="isAuthenticated" @submit.prevent="addComment">
        <v-textarea v-model="newComment" label="Add a comment" rows="3" required></v-textarea>
        <!-- <v-text-field v-model="newComment" label="Add a comment" required></v-text-field> -->
        <v-btn small outlined type="submit" color="primary"> 댓글 쓰기 </v-btn>
      </v-form>
      <v-alert v-else type="info" dismissible>Please log in to post a comment.</v-alert>

    </v-card>

    <v-alert v-model="alert" v-if="error" type="error" dismissible class="my-alert">{{ error.message }}</v-alert>
  </v-container>
</template>

<script>
import { db } from '../firebase';
import firebase from 'firebase/compat/app';
import sanitizeHtml from 'sanitize-html';
import { v4 as uuidv4 } from 'uuid'; // Importing uuidv4

export default {
  data() {
    return {
      post: null,
      comments: [],   // 댓글
      newComment: '',
      newReply: '',
      replyingTo: null,
      postContent: '',
      loading: false,
      error: null,
      alert: true   // 이것을 사용하지 않으면 alert가 dismissible를 누르면 다시 나타나지 않음
    };
  },
  computed: {
    isAuthenticated() {
      //return !!auth.currentUser;
      return this.$store.state.auth.user;
    }
  },
  async created() {
    this.loading = true;
    const postId = this.$route.params.id;
    try {
      const postRef = db.collection('posts').doc(postId);
      //const postDoc = await db.collection('posts').doc(postId).get();
      const postDoc = await postRef.get();
      if (postDoc.exists) {
        this.post = postDoc.data();
        this.postContent = this.sanitizeContent(this.post.content);
        this.loadComments(postId);  

        const today = new Date().toISOString().split('T')[0];
        let userId;
        const user = this.$store.state.auth.user;
        if (user) {
          //userId = auth.currentUser.uid;
          userId = user.id;
        } else {
          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
        }       
      } else {
        this.error = { message: 'Post not found' };
      }
    } catch (error) {
      this.error = error;
    } finally {
      this.loading = false;
    }
  },
  methods: {
    sanitizeContent(content) {
      return sanitizeHtml(content.replace(/\n/g, '<br>'), {
        allowedTags: ['b', 'i', 'em', 'strong', 'p', 'br'],
        allowedAttributes: {}
      });
    },
    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;
      }
    },
    async addComment() {
      // 댓글이 비었으면 저장하지 않습니다.      
      if(this.newComment === '') {
        this.error = { message: '댓글 내용이 없습니다.' };
        this.alert = true;
        return;
      }
      //const postId = this.post.id;
      const postId = this.$route.params.id;
      //const userName = auth.currentUser.displayName;
      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;
      }
    },
    async deleteComment(commentId) {
      const postId = this.$route.params.id;
      const commentRef = await db.collection('posts').doc(postId).collection('comments').doc(commentId);
      //const commentRef = db.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.');
        }
      }
    },    
    isCommentOwner(userId) {
      return userId === this.$store.state.auth.user.id;
    },
    async addReply(commentId) {
      if (!this.isAuthenticated) {
        this.error = { message: 'Please log in to reply.' };
        return;
      }

      if(this.newReply === '') {
        this.error = { message: '답글 내용이 없습니다.' };
        this.alert = true;
        return;
      }

      //const postId = this.post.id;
      //const userName = auth.currentUser.displayName;
      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(),
        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);
        this.newReply = '';
        this.replyingTo = null;

      } catch (error) {
        this.error = error;
      }
    },
    isReplyOwner(userId) {
      return userId === this.$store.state.auth.user.id;
    },
    async deleteReply(commentId, reply) {
      const postId = this.$route.params.id;
      //const commentRef = db.collection('comments').doc(commentId);
      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;
          }
        }
      }
    },
    showReplyForm(commentId) {
      this.replyingTo = commentId;
    },
    formatDate(date) {
      if (date && date.toDate) {
        return date.toDate().toLocaleString();
      }
      return '';
    },
    getOrCreateAnonymousUserId() {
      let userId = localStorage.getItem('anonymousUserId');
      if (!userId) {
        userId = uuidv4();
        localStorage.setItem('anonymousUserId', userId);
      }
      return userId;
    }
  }
};
</script>

<style scoped>
.my-alert {
  position: fixed;
  left: 10%;
  bottom: 30px;
  margin: 20px 0;
}
</style>