토이 프로젝트 - Vue, Firebase로 서버리스 PWA 개발

14. Vue(with Vuetify)와 Firebase로 서버리스 PWA myBlog 개발 - 블로그 글쓰기

그랜파 개발자 2025. 2. 14. 16:04

블로그 글쓰기

계정을 만들고, 사용자 인증, 글의 카테고리까지 만들었습니다.
이제 글쓰기 기능을 구현해야 할 때입니다.

1. 글쓰기 요구 사항

글을 쓰는 시나리오를 생각해 봅니다.

우선 로그인해야 합니다. 회원만 글을 쓸 수 있기 때문입니다.
Menu Drawer에 글쓰기 메뉴 항목 외에,
글쓰기 페이지를 열기 위한 아이콘도 화면 하단에 둘 생각입니다.
글쓰기 아이콘을 누르면 글쓰기 페이지가 열립니다.
글쓰기 페이지에 등록된 카테고리를 나타내어 선택할 수 있도록 합니다.
글의 제목과 내용을 쓰고 저장을 누르면 글이 저장됩니다.

2. 글의 데이터 항목

사용자는 카테고리를 선택하고,
글의 제목과 내용을 쓴 후 저장합니다.
글이 저장될 때는 어떤 것들이 추가될까요?
글의 카테고리, 제목, 내용 외에도
글쓴이의 userId, 이름, 글을 저장한 날, 글을 최종 수정한 날 등이 생각납니다.

DB :

  • firestore의 posts 컬렉션

posts 컬렉션의 문서 항목

  • category: 선택한 카테고리 이름
  • title: 사용자가 입력한 글의 제목
  • content: 사용자가 입력한 글의 내용
  • userId: 로그인한 사용자의 id
  • userName: 로그인한 사용자의 이름
  • createdAt: 글을 저장한 날짜와 시간
  • modifiedAt: 글의 최종 수정 날짜와 시간

새글 저장

async addPost({ commit }, post) {
    const docRef = await addDoc(collection(db, "posts"), post);
},

3. 글쓰기 기능 분석

글쓰기 페이지의 UI에 필요한 것들입니다.

카테고리 목록을 셀렉트 박스에 넣어 카테고리를 선택할 수 있고
제목을 입력할 수 있는 text field,
내용은 여러 줄의 입력이 필요하니 textarea를 사용하고
글을 저장할 수 있는 저장 버튼이 필요합니다.

4. 카테고리 로드

글쓰기 페이지를 열면 카테고리를 로드하여 셀렉트 박스에 넣어야 합니다.
카테고리는 로그인할 때 미리 로드 되어 있으므로
페이지가 열릴 때에는 로그된 카테고리에서 카테고리 이름만 배열로 추출하여
셀렉트 박스에 넣습니다.

computed: {
    . . .
    ...mapState('post', ['loading', 'categories']),
    . . .
  },

async mounted() {
    // 미리 state에 로드되어 있는 카테고리에서 이름만 가져온다.
    this.myCategories = this.categories.map(category => category.name);
    this.myCategories.unshift('카테고리 없음');
},
 async fetchCategories({ commit }, { userId }) { 
    if (!userId) return;

    try {
      // 사용자 ID로 필터링된 카테고리 가져오기
      const q = query(
        collection(db, "categories"),
        where("userId", "==", userId)
      );
      const querySnapshot = await getDocs(q);

      let categories = [];
      categories = querySnapshot.docs.map((doc) => ({
        id: doc.id, ...doc.data(),
      })); 
      // 로드한 카테고리를 상태에 저장한다.
      commit("setCatogories", categories); 
    } catch (error) {
      alert("Error fetching categories : " + error.message);
    }
  },

WriteView.vue

<!-- src/views/WriteView.vue -->
<template>
  <v-container>
    <v-card>
      <v-card-title>{{ getMyBlogName }}</v-card-title>

      <v-form class="pa-3 mt-n4" @submit.prevent="submitPost">

        <v-select v-model="selectedCategory" :items="myCategories" label="카테고리를 선택하세요"></v-select>

        <v-text-field v-model="title" label="제목" required></v-text-field>
        <v-textarea class="mt-n4" style="overflow-y: hidden;" v-model="content" label="내용" rows="5" required></v-textarea>

        <div class="mt-n3" style="text-align: right">
          <v-btn type="submit" color="primary">저장</v-btn>
        </div>

      </v-form>
    </v-card>

    <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 router from '@/router';  // Vue Router import

export default {
  data() {
    return {
      title: "",
      content: "",
      myCategories: [],
      selectedCategory: null, // 선택된 카테고리
    };
  },
  computed: {
    ...mapState('auth',['user', 'profile']),
    ...mapState('post', ['loading', 'categories']),
    getMyBlogName() {
      if(this.profile)
        return this.profile.blogName;
    }
  },
  methods: {
    ...mapActions('post', ['fetchCategories','addPost']),

    async submitPost() {
      if (!this.title || !this.content) {
        alert("Please fill in both fields.");
        return;
      }

      if(this.user) {        
        await this.addPost({
          category: this.selectedCategory,
          title: this.title,        // 사용자가 입력한 마이로그 제목
          content: this.content,    // 사용자가 입력한 마이로그 내용   
          userId: this.user.uid,    // 로그인한 사용자의 id
          userName: this.profile.name,  // 로그인한 사용자의 이름
          createdAt: new Date()     // 저장하는 현재 시간
        })        

        router.push("/");   // home으로
      }
    },
  },

  async mounted() {
    // 카테고리를 로드한다.
    await this.fetchCategories({userId: this.profile.userId});
    this.myCategories = this.categories.map(category => category.name);
  },

};
</script>