Vue로 PWA 개발

27. mylog 기본 워크플로우 완성

그랜파 개발자 2024. 10. 24. 14:56

마이로그를 쓰고(WriteMyLogView.vue), 마이로그들을 목록으로 조회하고(HomeView.vue), 마이로그를 목록에서 선택하여 내용을 확인(MyLogView.vue)하는 기본적인 워크플로우를 완성하였습니다.

1. 마이로그 접속 (main.js)

마이로그 페이지에 접속을 하면 마이로그는 저장된 모든 마이로그와 모든 회원 정보를 로드합니다. 현재 로그아웃 상태라면 회원의 정보는 로드하지 않지만, 이전에 로그인 상태였다면 접속할 때 자동으로 로그인을 합니다. 로그인을 하면 회원의 정보를 가지고 있습니다. 현재 회원 정보의 유무에 따라 로그인 여부를 알 수 있습니다.

2. 마이로그 홈페이지 HomeView.vue

홈페이지는 미리 로딩되어 있는 마이로그의 목록을 나열하고, 로그인 상태에 따른 메뉴 항목을 나타냅니다. 마이로그 목록에서 마이로그를 클릭하면 마이로그 상세 보기로 이동합니다.

<!-- src/views/HomeView.vue -->
<template>
  <v-container>
    <v-alert v-if="error" type="error" dismissible @input="resetErrorMsg">{{ error.message }}</v-alert>
    <v-progress-circular v-if="loading" indeterminate></v-progress-circular>
    <v-row justify="center">
      <v-col cols="12" md="12">
        <v-list>
          <v-list-item v-for="mylog in mylogs" :key="mylog.id" @click="viewMylog(mylog.id)">
            <v-list-item-content>
              <v-list-item-title>{{ mylog.title }}</v-list-item-title>
              <v-list-item-subtitle>{{ mylog.userName }} {{ mylog.createdAt.toDate().toLocaleString() }} ({{ mylog.views }}) </v-list-item-subtitle>
              <v-list-item-subtitle>{{ mylog.content }}</v-list-item-subtitle>
            </v-list-item-content>
          </v-list-item>
        </v-list>
        <v-alert v-if="!loading && !mylogs.length" type="info">마이로그가 없습니다.</v-alert>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
import { mapActions, mapGetters } from "vuex";

export default {
  data() {
    return {

    };
  },
  computed: {
  // mylogs : store의 state에 전체 마이로그를 로드하여 가지고 있다.
  // 홈페이지를 열면서 전체 마이로그를 가져온다.
    ...mapGetters('mylogs',['mylogs', 'error', 'loading'])
  },
  async created() {

  },
  methods: {
    ...mapActions('mylogs', ['resetError']),
    viewMylog(mylogId) {
      this.$router.push({ name: "MyLogView", params: { id: mylogId } });
    },
    resetErrorMsg() {
      this.resetError();
    }
  },
};
</script>

// src/store/modules/mylogs.js

// 저장된 전체 마이로그를 로드한다.
async fetchMylogs({ commit }) {
    commit('setLoading', true);
    try {
      const mylogs = [];
      const mylogsRef = query(collection(db, "mylogs"), orderBy("createdAt", "desc"));
      const querySnapshot = await getDocs(mylogsRef);
      querySnapshot.forEach((doc) => {
        mylogs.push({ id: doc.id, ...doc.data() });
      });
      commit('setMylogs', mylogs);  // store 저장한다.
    } catch (error) {
      console.log("error: ", error);
      commit('setError', error);
    } finally {
      commit('setLoading', false);
    }
  },

// --------------
const getters = {
  mylog: state => state.mylog,
  mylogs: state => state.mylogs,  
  error: state => state.error,
  loading: state => state.loading
};

3. 마이로그 쓰기 WriteMyLogView.vue

마이로그는 firesore DB에 mylogs collection에 저장되며 문서 id와, title, content, createdAt, useId, userName, views 필드를 가집니다. 문서 id는 DB에서 자동 생성되며, title과 content는 회원이 입력을 하지만 나머지 필드는 앱이 필요에 의해 생성하는 필드들입니다. 앞으로 개발이 진행하면서 필드들은 더 늘어날 것입니다.
회원이 작성한 마이로그를 firestore에 저장하면 웹앱은 다시 모든 마이로그를 로드하고 홈페이지로 이동합니다.

<!-- src/views/WriteMyLogView.vue -->
 <template>
  <v-container>
    <v-row justify="center">
      <v-col cols="12" md="8">
        <v-card>
          <v-card-title>
            <span class="text-h5">마이로그 - 내 일상의 기록</span>
          </v-card-title>

          <v-card-text>
            <v-form @submit.prevent="submitMylog">
              <v-text-field v-model="title" label="Title" required></v-text-field>
              <v-textarea v-model="content" label="Content" rows="10" required></v-textarea>
              <v-btn type="submit" color="primary">저장</v-btn>
            </v-form>
          </v-card-text>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
import { mapActions, mapGetters } from "vuex";

export default {
  data() {
    return {
      title: "",
      content: "",
    };
  },
  methods: {
    ...mapActions('mylogs', ['saveMylog', 'setError']),
    async submitMylog() {
      if (!this.title || !this.content) {
        alert("Please fill in both fields.");
        return;
      }

      const user = this.$store.state.auth.user;
      if(user) {
        try {
          const mylog = await this.saveMylog({
            title: this.title,        // 사용자가 입력한 마이로그 제목
            content: this.content,    // 사용자가 입력한 마이로그 내용   
            userId: user.id,          // 로그인한 사용자의 id
            userName: user.username,      // 로그인한 사용자의 이름
            createdAt: new Date()     // 저장하는 현재 시간
          })        
        } catch (error) {
          //console.error('Error adding user:', error);
          this.setError(error.message);       
        }
      }
    },
  },
};
</script>

<style scoped>
.text-h5 {
  font-weight: bold;
}
</style>

// src/store/modules/mylogs.js

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)
  }
},

4. 마이로그 상세 보기 (MyLogView.vue)

마이로그의 내용을 봅니다. 페이지가 열리면 mylogId에 해당하는 마이로그를 로드하여 화면에 나타냅니다. 글 내용에 HTML에 포함되어 있으므로 안전한 HTML로 변경하고, 조회수를 증가 시킵니다. 조회수는 로그인했든 하지 않았든 조회수를 증가시키는데 같은 사람이 하루에 몇번을 방문하든 하나만 증가시킵니다.
조회수 처리를 위하여 로그인하지 않았다면 익명의 userId를 얻어 사용자를 구분하고 로그인을 하였다면 회원의 로그인 Id를 사용합니다. 조회 정보는 별도의 views collection을 사용하여 문서 id, 회원 id에 대하여 조회날짜를 저장합니다.

<!-- 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>
        <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: '',
    };
  },
  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); // 조회수 업데이트 및 기록
  },  
  computed: {
    ...mapGetters('mylogs',['mylog'])   // 'mylog' mylogs store의 mylog getter로서 현재 선택된 마이로그를 돌려준다. 
  },
  methods: { 
    // 'fetchMylog' mylogs의 action 함수로 마이로그를 firestore 에서 읽어온다. 
    ...mapActions('mylogs', ['fetchMylog', 'resetError', 'updateViewCount']),

    // content를 안전한 html로 바꿔준다.
    sanitizeContent(content) {
      return sanitizeHtml(content.replace(/\n/g, '<br>'), {
        allowedTags: ['b', 'i', 'em', 'strong', 'p', 'br'],
        allowedAttributes: {}
      });
    },
    resetErrorMsg() {
      this.resetError();
    }
  }
};
</script>

// src/store/modules/mylogs.js

Copy// 조회 기록 업데이트 함수
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);
    }
  } 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;
      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);
  }
},

'Vue로 PWA 개발' 카테고리의 다른 글

29. mylog 모아보기  (0) 2024.10.25
28. mylog 수정  (0) 2024.10.25
26. mylog 익명 ID  (0) 2024.10.24
25. mylog 날짜별 조회수  (0) 2024.10.24
24. mylog 조회수 컬렉션  (0) 2024.10.23