블로그
저자의 글을 모아볼 수 있는 기능을 ‘블로그’라고 합시다.
Drawer 메뉴 항목의 ‘블로그’는 로그인 한 사용자의 글들을 모아 볼 수 있습니다.
글 상세보기에서 저자의 ‘블로그’ 이름을 눌러 블로그를 열면 그 글의 저자의 글들을 모아볼 수 있습니다.
내 블로그
저자 블로그
블로그
마이 블로그 요구 사항
로그인 후 메뉴를 통해서 ‘블로그’에 갈 수 있습니다.
Drawer 메뉴에서 블로그를 누르면
앱은 로그인 한 사용자의 userId로 작성한 글들을 목록을 보여줍니다.
상세 보기 페이지에서 저자의 블로그 이름을 눌러 ‘블로그’로 이동할 수도 있습니다.
블로그 페이지에는
블로그 이름과 블로그 주인이 등록한 카테고리 목록과
저자의 글들의 목록이 있습니다.
카테고리를 선택하면 선택한 카테고리의 글들을 보여줍니다.
최신순, 오래된순, 조회순을 선택하여 블로그 글을 정렬하여 볼 수 있습니다.
마이 블로그 요구 분석
- 메뉴에 ‘블로그’가 있어서 이것을 선택하면 로그인 사용자의 ‘블로그’ 페이지를 보여 줍니다.
- 상세보기에서 블로그 이름을 눌러 저자의 블로그로 이동할 수 있습니다.
- 블로그에는 블로그 이름, 저자가 등록한 카테고리 목록, 저자의 글들의 목록이 있습니다.
- 블로그 페이지가 열릴 때 userId에 대한 글들을 로드합니다.
- 사용자는 카테고리를 선택하여 카테고리 별로 글들을 볼 수 있습니다.
- 카테고리를 선택하면 글들을 카테고리 별로 필터링하여 보여 줍니다.
- 최신순, 오래된순, 조회수 순으로 정렬 방법을 선택하여 글들을 볼 수 있습니다.
- 정렬 방법을 선택하면 DB에서 정렬 방법대로 정렬하여 글들을 가져 옵니다.
- 글 목록에서 글을 선택하면 글 상세보기로 이동합니다.
BlogView 컴포넌트의 script
// 글 가져오기 - 최신순
viewByDateDesc() {
const userId = this.authorId;
if(this.selectedCategory == "전체 보기")
this.fetchBlogPosts({userId, category:null});
else
this.fetchBlogPosts({userId, category: this.selectedCategory});
},
// 글 가져오기 - 오래된 순
viewByDateAsc() {
const userId = this.authorId;
if(this.selectedCategory == "전체 보기")
this.fetchBlogPostsOrderByDateAsc({userId, category:null});
else
this.fetchBlogPostsOrderByDateAsc({userId, category: this.selectedCategory});
},
// 글 가져오기 - 조회순
viewByViews() {
const userId = this.authorId;
if(this.selectedCategory == "전체 보기")
this.fetchBlogPostsOrderByViews({userId, category:null});
else
this.fetchBlogPostsOrderByViews({userId, category: this.selectedCategory});
},
// 카테고리별 글 보기
onItemChange(newValue) {
const selectedItem = this.myCategories.find(item => item.id === newValue);
if (selectedItem) {
const category = selectedItem.category;
const userId = this.authorId;
if(category == "전체 보기")
this.fetchBlogPosts({userId, category:null});
else
this.fetchBlogPosts({userId, category});
}
},
store의 post모듈
// src/store/modules/post.js
import { v4 as uuidv4 } from 'uuid';
import { db, collection, getDocs, getDoc, setDoc, doc,
addDoc, updateDoc, deleteDoc, query, where, orderBy,
increment, arrayUnion } from "@/firebase";
const state = {
. . .
blogPosts: [], // 회원별 글 목록 - 블로그
};
const mutations = {
. . .
setBlogPosts(state, blogPosts) {
state.blogPosts = blogPosts;
},
};
const actions = {
. . .
// --- blog ----------------------
// 회원별 글목록(블로그) 불러오기 - 최신순 정열(기본)
async fetchBlogPosts({ commit }, {userId, category}) {
commit('setLoading', true);
if (userId) {
try {
let q;
if(category) {
q = query(
collection(db, "posts"),
where("userId", "==", userId),
where("category", "==", category),
orderBy("createdAt", "desc"));
} else {
q = query(
collection(db, "posts"),
where("userId", "==", userId),
orderBy("createdAt", "desc"));
}
const querySnapshot = await getDocs(q);
const blogPosts = [];
querySnapshot.forEach((doc) => {
blogPosts.push({ id: doc.id, ...doc.data() });
});
commit('setBlogPosts', blogPosts); // 상태에 저장
} catch (error) {
alert("블로그 로드 실패:" + error.message);
console.error("Error fetching categories:", error);
}
}
commit('setLoading', false);
},
// 회원별 글목록(블로그) 불러오기 - 오래된순 정렬
async fetchBlogPostsOrderByDateAsc({ commit }, {userId, category}) {
commit('setLoading', true);
if (userId) {
try {
let q;
if(category) {
q = query(
collection(db, "posts"),
where("userId", "==", userId),
where("category", "==", category),
orderBy("createdAt", "asc"));
} else {
q = query(
collection(db, "posts"),
where("userId", "==", userId),
orderBy("createdAt", "asc"));
}
const querySnapshot = await getDocs(q);
const blogPosts = [];
querySnapshot.forEach((doc) => {
blogPosts.push({ id: doc.id, ...doc.data() });
});
commit('setBlogPosts', blogPosts); // 상태에 저장
} catch (error) {
alert("블로그 로드 실패 + " + error.message);
console.error("Error fetching categories:", error);
}
}
commit('setLoading', false);
},
// 회원별 글목록(블로그) 불러오기 - 조회순 정렬
async fetchBlogPostsOrderByViews({ commit }, {userId, category}) {
commit('setLoading', true);
if (userId) {
try {
let q;
if(category) {
q = query(
collection(db, "posts"),
where("userId", "==", userId),
where("category", "==", category),
orderBy("views", "desc"));
} else {
q = query(
collection(db, "posts"),
where("userId", "==", userId),
orderBy("views", "desc"));
}
const querySnapshot = await getDocs(q);
const blogPosts = [];
querySnapshot.forEach((doc) => {
blogPosts.push({ id: doc.id, ...doc.data() });
});
commit('setBlogPosts', blogPosts); // 상태에 저장
} catch (error) {
alert("블로그 로드 실패:" + error.message);
console.error("Error fetching categories:", error);
}
}
commit('setLoading', false);
},
// -------------------------------------------------
};
const getters = {
};
export default {
namespaced: true,
state,
mutations,
actions,
getters
};
BlogView.vue
<!-- src/views/BlogView.vue -->
<template>
<v-container>
<v-row>
<v-col><v-card-title style="font-size:1em">{{ getBlogName }}</v-card-title></v-col>
</v-row>
<v-row class="mt-n5">
<v-col >
<v-select class="mt-2" v-model="selectedCategory" :items="myCategories"
label="카테고리를 선택하세요" item-value="id" item-text="category" @change="onItemChange">
</v-select>
<span style="text-align: right">
<v-btn text @click="viewByDateDesc">최신순</v-btn>
<v-btn text @click="viewByDateAsc">오래된순</v-btn>
<v-btn text @click="viewByViews">조회순</v-btn>
</span>
</v-col>
</v-row>
<v-row v-if="blogPosts.length > 0">
<v-col v-for="post in blogPosts" :key="post.id" @click="goToPostView(post.id)" cols="12">
<v-card>
<!--카테고리-->
<v-card-text class="mb-n5" v-if="post.category" style="font-size:1em">
{{ post.category }}
</v-card-text>
<v-card-text class="mb-n5" v-else style="font-size:1em">
카테고리 없음
</v-card-text>
<v-card-title style="font-size:1em">{{ post.title }}</v-card-title>
<!-- eslint-disable -->
<v-card-text style="font-size:1em" class="mt-n2 mb-n6" v-html="sanitizeContent(post.content)"></v-card-text>
<!-- eslint-enable -->
<v-card-subtitle>
<span v-if="post.commentCount> 0" style="cursor: pointer">댓글 {{post.commentCount }} </span>
{{ post.userName }} . {{ formatDate(post.createdAt) }}
<span v-if="post.views > 0" > ({{ post.views }})</span>
</v-card-subtitle>
</v-card>
</v-col>
</v-row>
<div class="text-center">
<v-progress-circular v-if="loading" indeterminate></v-progress-circular>
</div>
</v-container>
</template>
<script>
import { mapActions, mapState } from "vuex";
import sanitizeHtml from 'sanitize-html';
export default {
data() {
return {
authorId: null, // 글쓴이
myCategories: [],
selectedCategory: 0, // 선택된 카테고리
};
},
computed: {
...mapState('auth', ['user', 'profile', 'profiles']),
...mapState('post', ['loading', 'categories', 'blogPosts']),
getBlogName() {
// authorId로 프로파일 정보를 찾아야 한다.
// 글 저자 찾기 - 상태에 로드되어 있는 전체 계정 설정 정보를 이용한다.
const authorProfile = this.profiles.find(profile => profile.uids.includes(this.authorId));
if(authorProfile)
return authorProfile.blogName;
else
return "";
}
},
methods: {
...mapActions('post', ['fetchBlogPosts', 'fetchBlogPostsOrderByDateAsc',
'fetchBlogPostsOrderByViews', 'fetchCategories']),
goToPostView(postId) {
this.$router.push({ name: 'Post', params: { id: postId } });
},
// 글 가져오기 - 최신순
viewByDateDesc() {
const userId = this.authorId;
if(this.selectedCategory == "전체 보기")
this.fetchBlogPosts({userId, category:null});
else
this.fetchBlogPosts({userId, category: this.selectedCategory});
},
// 글 가져오기 - 오래된 순
viewByDateAsc() {
const userId = this.authorId;
if(this.selectedCategory == "전체 보기")
this.fetchBlogPostsOrderByDateAsc({userId, category:null});
else
this.fetchBlogPostsOrderByDateAsc({userId, category: this.selectedCategory});
},
// 글 가져오기 - 조회순
viewByViews() {
const userId = this.authorId;
if(this.selectedCategory == "전체 보기")
this.fetchBlogPostsOrderByViews({userId, category:null});
else
this.fetchBlogPostsOrderByViews({userId, category: this.selectedCategory});
},
// content를 안전한 html로 바꿔준다.
sanitizeContent(content) {
return sanitizeHtml(content.replace(/\n/g, '<br>'), {
allowedTags: ['b', 'i', 'em', 'strong', 'p', 'br'],
allowedAttributes: {}
});
},
formatDate(date) {
if (date && date.toDate) {
return date.toDate().toISOString().replace('T', ' ').substring(0, 16);
}
return '';
},
onItemChange(newValue) {
const selectedItem = this.myCategories.find(item => item.id === newValue);
if (selectedItem) {
const category = selectedItem.category;
const userId = this.authorId;
if(category == "전체 보기")
this.fetchBlogPosts({userId, category:null});
else
this.fetchBlogPosts({userId, category});
}
},
},
async mounted() {
// 파라미터로 userId가 넘어오지 않으면
// 메뉴에서 선택한 것이므로 로그인한 회원의 블로그이다.
// 그렇지 않으면 파라미터로 넘어온 회원의 블로그이다.
this.authorId = this.$route.params.userId;
//console.log('authorId: ', this.authorId);
if(this.authorId == 'myblog') {
// my blog의 경우
this.authorId = this.user.uid;
}
// 카테고리는 필요한 컴포넌트에서 로드한다.
await this.fetchCategories({userId: this.authorId});
// 카테고리 이름만 가져온다.
// 셀렉트 박스에 사용할 ID는 배열을 만들면서 추가한다.
this.myCategories = this.categories.map((category, index) => ({
id: index+1, category: category.name
}));
this.myCategories.unshift({id:0, category: '전체 보기'});
// userId별 글가져오기
await this.fetchBlogPosts({userId: this.authorId, category:null});
},
};
</script>
'토이 프로젝트 - Vue, Firebase로 서버리스 PWA 개발' 카테고리의 다른 글
23. 프론트엔드 서버리스 프로젝트 PWA myBlog 개발 - 블로그 조회수 (0) | 2025.02.28 |
---|---|
22. Vue 프로젝트 PWA myBlog 개발 - 블로그 구독 (1) | 2025.02.27 |
21. 서버리스 PWA myBlog 개발 - 게시글 댓글에 대한 답글 (0) | 2025.02.26 |
20. Firestore로 PWA myBlog 개발 - 댓글 쓰기 (0) | 2025.02.25 |
19. Firestore로 PWA myBlog 개발 - 글수정 (0) | 2025.02.24 |