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