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>
<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 |