PWA

ChatGPT와 함께 PWA Blog 개발 - 댓글

그랜파 개발자 2024. 8. 23. 04:35

59. 댓글

ChatGPT에게 물었습니다.

I want to leave a comment.

댓글을 쓰고 싶어.

To implement a comment feature in your blog application using Vue, Vuex, Vuetify, and Firestore, you need to follow these steps:
Vue, Vuex, Vuetify 및 Firestore를 사용하여 블로그 애플리케이션에 댓글 기능을 구현하려면 다음 단계를 따라야 합니다.

  1. Add Comment Form: Create a form for users to submit comments.
    댓글 양식 추가: 사용자가 댓글을 제출할 수 있는 양식을 만듭니다.
  2. Store Comments in Firestore: Save comments in Firestore under each post.
    Firestore에 댓글 저장: 각 게시물 아래에 Firestore에 댓글을 저장합니다.
  3. Display Comments: Retrieve and display comments for each post.
    댓글 표시: 각 게시물에 대한 댓글을 검색하고 표시합니다.

Step-by-Step Implementation

단계별 구현

Step 1: Add Comment Form

1단계: 댓글 양식 추가

Create a form in your PostDetailView.vue component to allow users to submit comments.
사용자가 댓글을 제출할 수 있도록 PostDetailView.vue 구성 요소에 양식을 만듭니다.

<template>
  <v-container>
    <v-alert v-if="error" type="error" dismissible>{{ error.message }}</v-alert>
    <v-progress-circular v-if="loading" indeterminate></v-progress-circular>
    <div v-if="post">
      <h1>{{ post.title }}</h1>
      <div>{{ post.userName }} - {{ formatDate(post.createdAt) }}</div>
      <div>{{ post.content }}</div>
      <div>Views: {{ post.views }}</div>

      <!-- 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>
            <v-list-item-title>{{ comment.userName }}</v-list-item-title>
            <v-list-item-subtitle>{{ comment.content }}</v-list-item-subtitle>
          </v-list-item-content>
        </v-list-item>
      </v-list>

      <!-- Add Comment Form -->
      <v-form @submit.prevent="addComment">
        <v-text-field
          v-model="newComment"
          label="Add a comment"
          required
        ></v-text-field>
        <v-btn type="submit" color="primary">Submit</v-btn>
      </v-form>
    </div>
  </v-container>
</template>

<script>
import { db, auth } from '../firebase';
import firebase from 'firebase/app';
import { v4 as uuidv4 } from 'uuid';

export default {
  data() {
    return {
      post: null,
      comments: [],
      newComment: '',
      loading: false,
      error: null
    };
  },
  async created() {
    const postId = this.$route.params.id;
    this.loading = true;
    try {
      const postRef = db.collection('posts').doc(postId);
      const postDoc = await postRef.get();
      if (postDoc.exists) {
        this.post = { id: postDoc.id, ...postDoc.data() };
        this.loadComments(postId);

        const today = new Date().toISOString().split('T')[0];
        let userId;

        if (auth.currentUser) {
          userId = auth.currentUser.uid;
        } 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: {
    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() {
      const postId = this.post.id;
      const userName = auth.currentUser ? auth.currentUser.displayName : 'Anonymous';
      const comment = {
        content: this.newComment,
        userName,
        createdAt: new Date()
      };
      try {
        await db.collection('posts').doc(postId).collection('comments').add(comment);
        this.comments.push(comment);
        this.newComment = '';
      } catch (error) {
        this.error = error;
      }
    },
    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>
/* Add any additional styling here */
</style>

Explanation 설명

1. Template:

  • Added a section to display existing comments.
    기존 댓글을 표시하는 섹션을 추가했습니다.
  • Added a form to submit new comments.
    새로운 의견을 제출하는 양식을 추가했습니다.

2. Data:

  • comments: Array to store the comments.
    comments: 댓글을 저장할 배열입니다.
  • newComment: Model for the comment input field.
    newComment: 댓글 입력 필드의 모델입니다

3. Methods:

  • loadComments: Loads comments from Firestore for the current post.
    loadComments: Firestore에서 현재 게시물에 대한 댓글을 로드합니다.
  • addComment: Adds a new comment to the Firestore comments collection and updates the local comments array.
    addComment: Firestore 댓글 컬렉션에 새 댓글을 추가하고 로컬 댓글 배열을 업데이트합니다.

4. Created Hook:

  • Loads the post and its comments when the component is created.
    컴포넌트 생성 시 게시물과 해당 댓글을 로드합니다.

Step 2: Store Comments in Firestore

2단계: Firestore에 댓글 저장

Comments are stored in a sub-collection comments under each post document in Firestore. The addComment method handles adding new comments to this sub-collection.
댓글은 Firestore의 각 게시물 문서 아래 하위 컬렉션 'comments'에 저장됩니다. addComment 메소드는 이 하위 컬렉션에 새 댓글 추가를 처리합니다.

Step 3: Display Comments

3단계: 댓글 표시

Comments are retrieved from the Firestore sub-collection and displayed in the template using a v-list.
댓글은 Firestore 하위 컬렉션에서 검색되고 'v-list'를 사용하여 템플릿에 표시됩니다.

By following these steps, you have implemented a comment feature where users can leave comments on posts, and these comments are displayed along with the post details.

이러한 단계를 수행하면 사용자가 게시물에 댓글을 남길 수 있는 댓글 기능이 구현되었으며, 이러한 댓글은 게시물 세부정보와 함께 표시됩니다.

ChatGPT에게 물었습니다.

Please allow only logged in members to post comments.

로그인 한 회원만 댓글을 쓸수 있도록 해줘.

To allow only logged-in members to post comments, we can check the authentication state before allowing the submission of a comment. If the user is not authenticated, we can display a message prompting them to log in.
로그인한 회원만 댓글을 작성할 수 있도록 댓글 작성을 허용하기 전에 인증 상태를 확인할 수 있습니다. 사용자가 인증되지 않은 경우 로그인하라는 메시지를 표시할 수 있습니다.

Step-by-Step Implementation

단계별 구현

  1. Check Authentication State: Check if the user is logged in before showing the comment form.
    인증 상태 확인: 댓글 양식을 표시하기 전에 사용자가 로그인되어 있는지 확인하세요.
  2. Prompt Login: If the user is not logged in, show a prompt to log in.
    프롬프트 로그인: 사용자가 로그인되어 있지 않은 경우 로그인하라는 프롬프트를 표시합니다.

Updated PostDetailView.vue

<template>
  <v-container>
    <v-alert v-if="error" type="error" dismissible>{{ error.message }}</v-alert>
    <v-progress-circular v-if="loading" indeterminate></v-progress-circular>
    <div v-if="post">
      <h1>{{ post.title }}</h1>
      <div>{{ post.userName }} - {{ formatDate(post.createdAt) }}</div>
      <div>{{ post.content }}</div>
      <div>Views: {{ post.views }}</div>

      <!-- 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>
            <v-list-item-title>{{ comment.userName }}</v-list-item-title>
            <v-list-item-subtitle>{{ comment.content }}</v-list-item-subtitle>
          </v-list-item-content>
        </v-list-item>
      </v-list>

      <!-- Add Comment Form -->
      <v-form v-if="isAuthenticated" @submit.prevent="addComment">
        <v-text-field
          v-model="newComment"
          label="Add a comment"
          required
        ></v-text-field>
        <v-btn type="submit" color="primary">Submit</v-btn>
      </v-form>
      <v-alert v-else type="info" dismissible>Please log in to post a comment.</v-alert>
    </div>
  </v-container>
</template>

<script>
import { db, auth } from '../firebase';
import firebase from 'firebase/app';
import { v4 as uuidv4 } from 'uuid';

export default {
  data() {
    return {
      post: null,
      comments: [],
      newComment: '',
      loading: false,
      error: null
    };
  },
  computed: {
    isAuthenticated() {
      return !!auth.currentUser;
    }
  },
  async created() {
    const postId = this.$route.params.id;
    this.loading = true;
    try {
      const postRef = db.collection('posts').doc(postId);
      const postDoc = await postRef.get();
      if (postDoc.exists) {
        this.post = { id: postDoc.id, ...postDoc.data() };
        this.loadComments(postId);

        const today = new Date().toISOString().split('T')[0];
        let userId;

        if (auth.currentUser) {
          userId = auth.currentUser.uid;
        } 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: {
    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() {
      const postId = this.post.id;
      const userName = auth.currentUser.displayName;
      const comment = {
        content: this.newComment,
        userName,
        createdAt: new Date()
      };
      try {
        await db.collection('posts').doc(postId).collection('comments').add(comment);
        this.comments.push(comment);
        this.newComment = '';
      } catch (error) {
        this.error = error;
      }
    },
    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>
/* Add any additional styling here */
</style>

Explanation 설명

1. Template

  • Added a v-if="isAuthenticated" directive to the comment form, so it only appears if the user is authenticated.
    댓글 양식에 v-if="isAuthenticated" 지시문을 추가하여 사용자가 인증된 경우에만 표시됩니다.
  • Added a v-alert message to prompt users to log in if they are not authenticated.
    사용자가 인증되지 않은 경우 로그인하라는 메시지를 표시하는 'v-alert' 메시지가 추가되었습니다.

2. Computed Property:

  • Added isAuthenticated computed property to determine if a user is logged in.
    사용자가 로그인했는지 확인하기 위해 isAuthenticated 계산 속성을 추가했습니다.

3. Methods:

  • The addComment method now only adds comments if the user is authenticated.
    addComment 메소드는 이제 사용자가 인증된 경우에만 댓글을 추가합니다.

By following these steps, you ensure that only logged-in users can post comments. If a user is not logged in, they will see a message prompting them to log in.
다음 단계를 따르면 로그인한 사용자만 댓글을 게시할 수 있습니다. 사용자가 로그인하지 않은 경우 로그인하라는 메시지가 표시됩니다.

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

PostDetailView.vue 수정

실행

Source Code

src/viewa/PostDetailView.vue

Copy<!-- src/viewa/PostDetailView.vue -->
<template>
  <v-container>
    <v-alert v-if="error" type="error" dismissible>{{ error.message }}</v-alert>
    <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) }}</v-card-text>
      <v-card-text v-html="postContent"></v-card-text>
      <v-card-text>Views: {{ post.views }}</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>
            <v-list-item-title>{{ comment.content}}</v-list-item-title>
            <v-list-item-subtitle>{{ comment.userName }}</v-list-item-subtitle>
          </v-list-item-content>
        </v-list-item>
      </v-list>

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

    </v-card>
  </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: '',
      postContent: '',
      loading: false,
      error: null
    };
  },
  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() {
      //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,
        userName,
        createdAt: new Date()
      };
      try {
        await db.collection('posts').doc(postId).collection('comments').add(comment);
        this.comments.push(comment);
        this.newComment = '';
      } catch (error) {
        this.error = error;
      }
    },
    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>