Vue로 PWA 개발

8. Mylog 계정 만들기 - 완성

그랜파 개발자 2024. 10. 9. 13:30

계정 만들기 폼에서 사용자의 정보를 입력 받아 이메일과 패스워드로 Firebase 인증 서비스를 이용하여 구글 계정을 생성하고, 구글 계정의 uid를 받아 mylog의 계정 DB에 계정 정보를 저장합니다. 사용자 인증은 구글 계정을 이용하므로 사용자의 비밀번호는 mylog DB에 저장되지 않습니다.
등록된 계정 정보는 계정 정보 페이지로 확인할 수 있고, 계정 정보 페이지에서 수정할 수도 있습니다.

1. UI

2. Source Code

src/firebase.js

// src/firebase.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore, collection, addDoc, getDocs, query, where, updateDoc, doc } from "firebase/firestore";

const firebaseConfig = {
  apiKey: process.env.VUE_APP_FIREBASE_API_KEY,
  authDomain: process.env.VUE_APP_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.VUE_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.VUE_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.VUE_APP_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.VUE_APP_FIREBASE_APP_ID,
};

// npm install dotenv - env가 정상 동작하지 않을 때 설치 필요함

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);

export { auth, db, collection, addDoc, getDocs, query, where, updateDoc, doc };

src/main.js

// src/main.js

import Vue from 'vue'
import App from './App.vue'
import './registerServiceWorker'
import router from './router'
import store from './store'
import vuetify from './plugins/vuetify'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  vuetify,
  render: h => h(App),
  created() {
    // Set up Firebase auth state change listener
    const { dispatch } = this.$store;
    // Initialize Firebase authentication to check for the logged-in user
    dispatch('auth/initializeAuth');
  }
}).$mount('#app')

src/store/modules/auth.js

// src/store/modules/auth.js

import router from '@/router';  // Vue Router import
import { auth, db, collection, getDocs, query, where, updateDoc, doc } from "@/firebase";
import { createUserWithEmailAndPassword, onAuthStateChanged } from "firebase/auth";

const state = {
  user: null,
  userInfo: null,
  users: [],
  isLoading: false,
  error: null
};

const mutations = {
  setUser(state, user) {
    state.user = user;
  },
  setUserInfo(state, userInfo) {
    state.userInfo = userInfo;
  },
  setUsers(state, users) {
    state.users = users;
    state.isLoading = false;
  },
  setError(state, error) {
    state.error = error;
    state.isLoading = false;
  },
  setLoading(state, isLoading) {
    state.isLoading = isLoading;
  }
};

const actions = {
  async fetchUsers({ commit }) {
    commit('setLoading', true);
    try {
      const users = [];
      const userRef = collection(db, "users");
      const querySnapshot = await getDocs(userRef);
      querySnapshot.forEach((doc) => {
        // doc.data() is never undefined for query doc snapshots
        users.push({ id: doc.id, ...doc.data() });
      });
      commit('setUsers', users);
    } catch (error) {
      commit('setError', error.message);
    }
  },
  async addUser({ dispatch, commit }, user) {
    try {
      // Save additional user data to Firestore
      const userRef = collection(db, "users");
      const newUser = await addDoc(userRef, {
        email: user.email,
        uids: user.uids,
        username: user.username,
        mylogname: user.mylogname
      });
      
      // 로그인 설정
      user.id = newUser.id;
      commit('setUser', user);
      dispatch('fetchUsers');

      commit("setError", null);
    } catch (error) {
      console.error('Error adding user:', error);
    }
  },
  
  async register({ commit, dispatch }, { email, password, username, mylogname }) {
    try {
      const userCredential = await createUserWithEmailAndPassword(auth, email, password);

      const user = userCredential.user;        
      const newUser = {
        email: email,
        uids: [user.uid],
        username: username,
        mylogname: mylogname
      };
      
      //console.log('newuser:', newUser);
      dispatch('addUser', newUser);

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

    } catch (error) {
      commit("setError", error.message);
    }
  },

  async fetchUserInfo({ commit, state }, user) {

    // if (!state.user) return;

    // try {
    //   const q = query(collection(db, "users"), where("uid", "==", state.user.uid));
    //   const querySnapshot = await getDocs(q);
    //   querySnapshot.forEach((doc) => {
    //     commit("setUserInfo", { ...doc.data(), id: doc.id });
    //   });
    // } catch (error) {
    //   commit("setError", error.message);
    // }

    try {
      commit("setUserInfo", user);
    } catch (error) {
      commit("setError", error.message);
    }
  },
  async updateUserInfo({ commit }, updatedInfo) {
    try {
      const userDoc = doc(db, "users", updatedInfo.id);
      await updateDoc(userDoc, updatedInfo);
      
      commit("setUserInfo", updatedInfo);
      commit("setUser", updatedInfo);

      commit("setError", null);
    } catch (error) {
      commit("setError", error.message);
    }
  },
  async fetchUserWithUid({ commit, dispatch }, {uid}) {
    // 로그인은 google 계정으로 한다.
    // user 정보는 mylog 계정 정보를 사용한다.
    // 그러므로 구글 계정의 uid로 mylog 계정의 user 정보를 가져와야 한다.
    try {
      const users = []; 
      const userRef = query(collection(db, "users"), where('uids', 'array-contains', uid));
      const querySnapshot = await getDocs(userRef);
      querySnapshot.forEach((doc) => {
        // doc.data() is never undefined for query doc snapshots
         users.push({ id: doc.id, ...doc.data() });
      });
      // 로그인 설정을 한다.
      commit('setUser', users[0]);
      dispatch("fetchUserInfo", users[0]);
    } catch (error) {
      console.error('Error fetching user:', error);
    }            
  },

  initializeAuth({ commit, dispatch }) {
    onAuthStateChanged(auth, (user) => {
      if (user) {
        // user.uid로 웹앱의 firestore DB에서 계정 정보를 가져온다.
        dispatch('fetchUserWithUid', {uid: user.uid});
        // fetchUserWithUid에서 setUser를 실행한다.
        // commit("setUser", user);
        //dispatch("fetchUserInfo");
      } else {
        commit("setUser", null);
        commit("setUserInfo", null);
      }
    });
  },
};

const getters = {
  user: state => state.user, 
  userInfo: state => state.userInfo,  
  users: state => state.users,
  error: state => state.error,
  isAuthenticated: state => !!state.user,
  isLoading: state => state.isLoading
};

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
};

src/store/index.js

// src/store/index.js

import Vue from 'vue';
import Vuex from 'vuex';
import auth from './modules/auth';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    auth
  }
});

src/views/RegisterView.vue

<!-- src/views/RegisterView.vue -->
<template>
  <!-- v-container class="fill-height" fluid>
    <v-row align="center" justify="center" -->
  <v-container fluid>
    <v-row align="center" justify="center" class="mt-4">
      <v-col cols="12" sm="8" md="4">
        <v-card>
          <v-card-title class="text-h5"> 계정 만들기 </v-card-title>

          <v-card-text>
            <v-form ref="form" v-model="valid" lazy-validation @submit.prevent="userRegister">
              <v-text-field v-model="username" :rules="usernameRules" label="이름" prepend-icon="mdi-account" required ></v-text-field>
              <v-text-field v-model="mylogname" :rules="usernameRules" label="마이로그 이름" prepend-icon="mdi-post" required></v-text-field>
              <v-text-field v-model="email" :rules="emailRules" label="이메일" prepend-icon="mdi-email" required></v-text-field>
              <v-text-field v-model="password" :rules="passwordRules" label="비밀번호" prepend-icon="mdi-lock" type="password" required></v-text-field>
              <v-text-field v-model="confirmPassword" :rules="confirmPasswordRules" label="비밀번호 확인" prepend-icon="mdi-lock" type="password" required></v-text-field>

              <v-progress-circular v-if="isLoading" indeterminate :width="7" :size="70" color="grey lighten-1"></v-progress-circular>  

              <v-btn :disabled="!valid" color="primary" type="submit" class="mr-4" > 계정 만들기 </v-btn>
              <v-btn color="secondary" @click="clear"> 지우기 </v-btn>
              <v-alert v-if="error" type="error" class="mt-3" dismissible>{{ error }}</v-alert>

            </v-form>
          </v-card-text>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

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

export default {
  
  data() {
    return {
      valid: false,
      username: '',
      mylogname: '',
      email: '',
      password: '',
      confirmPassword: '',
      usernameRules: [
        (v) => !!v || 'Username is required',
        (v) => (!!v && v.length >= 3) || 'Username must be at least 3 characters',
      ],
      emailRules: [
        (v) => !!v || 'Email is required',
        (v) => (!!v && /.+@.+\..+/.test(v)) || 'E-mail must be valid',
      ],
      passwordRules: [
        (v) => !!v || 'Password is required',
        (v) => (!!v && v.length >= 6) || 'Password must be at least 6 characters',
      ],
      confirmPasswordRules: [
        (v) => !!v || 'Password confirmation is required',
        (v) => (!!v && v === this.password) || 'Passwords do not match',
      ],
    };
  },
  computed: {
    ...mapGetters('auth', ['error', 'isLoading']),
  },
  methods: {
    ...mapActions('auth', ['register']),
    async userRegister() {
      if (this.$refs.form.validate()) {
        await this.register({ 
          email: this.email, 
          password: this.password,
          username: this.username, 
          mylogname: this.mylogname 
        });
      }
    },
    clear() {
      this.$refs.form.reset();
      this.password = '';
      this.confirmPassword = '';
    },
  },
};
</script>

<style scoped>
.fill-height {
  min-height: 100vh;
}
</style>

src/views/ProfileView.vue

<!-- src/views/ProfileView.vue -->
<template>
  <v-container>
    <v-row justify="center">
      <v-col cols="12" md="8" lg="6">
        <v-card class="mx-auto" outlined>
          <v-card-title>
            <span class="text-h6">계정 정보</span>
          </v-card-title>

          <v-card-text>
            <v-form ref="form" v-model="valid" lazy-validation>

              <!-- Email Field (Non-Editable) -->
              <v-text-field label="이메일" v-model="editableUserInfo.email" disabled readonly></v-text-field>
              <!-- usernam Field -->
              <v-text-field label="이름" v-model="editableUserInfo.username" :rules="[rules.required]"></v-text-field>
              <!-- mylogname Field -->
              <v-text-field label="마이로그 이름" v-model="editableUserInfo.mylogname" :rules="[rules.required]"></v-text-field>
              <!-- Age Field -->
              <!-- <v-text-field label="Age" v-model="editableUserInfo.age" :rules="[rules.required, rules.numeric]" type="number" ></v-text-field> -->
            </v-form>
          </v-card-text>

          <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn color="primary" @click="saveChanges" :disabled="!valid">수정</v-btn>
            <v-btn color="grey" @click="cancelEdit">취소</v-btn>
          </v-card-actions>

          <v-card-text v-if="error" class="error--text">{{ error }}</v-card-text>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

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

export default {
  data() {
    return {
      editableUserInfo: {
        email: "",
        username: "",
        mylogname: "",
        id: "", // Add an id to identify the document in Firestore
      },
      valid: false,
      rules: {
        required: (value) => !!value || "Required.",
        numeric: (value) => !isNaN(parseInt(value)) || "Must be a number.",
      },
    };
  },
  computed: {
    ...mapGetters('auth', ["userInfo", "error"]),
    getUserInfo() {
      return this.userInfo;
    },
    getError() {
      return this.error;
    },
  },
  watch: {
    getUserInfo: {
      handler(newValue) {
        console.log('newValue : ', newValue);
        this.editableUserInfo = { ...newValue }; // Clone the userInfo to editableUserInfo
      },
      immediate: true,
    },
  },
  methods: {
    ...mapActions('auth', ["fetchUserInfo", "updateUserInfo"]),
    saveChanges() {
      this.updateUserInfo(this.editableUserInfo);
    },
    cancelEdit() {
      this.editableUserInfo = { ...this.userInfo }; // Reset form to original data
    },
  },
  created() {
    const user = this.$store.state.auth.user;
    this.editableUserInfo = user;
    //this.fetchUserInfo(); // Fetch user info when the component is created
    
  },
};
</script>

<style scoped>
.error--text {
  color: red;
}
</style>