PWA

사용자 인증 - 소스코드

그랜파 개발자 2024. 8. 10. 16:21

49. 몇가지 개선 사항과 소스 코드

개선 1.

계정 정보에서 구글 계정 연동이 되어 있는 경우에도 Google 계정 연동‘’을 누르면 계정 연동을 합니다. 이 경우 구글 연동을 또 하지 않고 알림 메시지를 나타내도록 합니다.

개선 2.

알림 메시지를 나타낸 후에 메시지 창을 닫으면 메시지가 남아 있어서 화면에 계속 나타납니다. 이 경우 알림 메시지 창을 닫으면 알림 메시지를 제거합니다.

개선 3.

버튼과 경고 메시지 창이 붙어 있습니다. 이것에 여백을 두도록 합니다.

소스코드

알림 메시지 출력

알림 메시지 제거

ChatGPT에게 물었습니다.

<v-alert v-if="error" type="error" dismissible >{{ error }}</v-alert> 에서 에러 메시지 리셋 방법은?

 

To reset the error message in a Vue component when using {{ error }}, you need to ensure that the error state is set to null or an empty string after the alert is dismissed. Here’s a step-by-step example of how to achieve this:
<v-alert v-if="error" type="error" 해제 가능>{{ error }}</v-alert>를 사용할 때 Vue 구성 요소에서 오류 메시지를 재설정하려면 오류가 발생하는지 확인해야 합니다. 상태는 경고가 해제된 후 'null' 또는 빈 문자열로 설정됩니다. 이를 달성하는 방법에 대한 단계별 예는 다음과 같습니다.

Step-by-Step Guide

  1. Define the error state in your component's data.
    구성 요소 데이터의 오류 상태를 정의합니다.
  2. Add a method to reset the error.
    오류를 재설정하는 방법을 추가하세요.
  3. Bind the dismissible functionality of the alert to the method that resets the error.
    경고의 해제 가능한 기능을 오류를 재설정하는 메서드에 바인딩합니다.

Example

Here’s a complete example of a Vue component with an error message that can be reset:
다음은 재설정할 수 있는 오류 메시지가 있는 Vue 구성 요소의 전체 예입니다.

<template>
  <v-container>
    <v-form @submit.prevent="submitForm">
      <v-text-field v-model="email" label="Email" required></v-text-field>
      <v-text-field v-model="password" label="Password" type="password" required></v-text-field>
      <v-btn type="submit">Submit</v-btn>
    </v-form>
    <v-alert
      v-if="error"
      type="error"
      dismissible
      @input="resetError"
    >
      {{ error }}
    </v-alert>
  </v-container>
</template>

<script>
export default {
  data() {
    return {
      email: '',
      password: '',
      error: null
    };
  },
  methods: {
    async submitForm() {
      try {
        // Simulate form submission and error
        throw new Error('An error occurred!');
      } catch (e) {
        this.error = e.message;
      }
    },
    resetError() {
      this.error = null;
    }
  }
};
</script>

<style scoped>
/* Add any component-specific styles here */
</style>

Explanation 설명

1. Data Object:

  • email, password, and error are defined in the data object. The error field will hold the error message.
    email, password, error는 data 객체에 정의되어 있습니다. 'error' 필드에는 오류 메시지가 저장됩니다.

2. submitForm Method:

  • This method simulates form submission. If an error occurs, it sets the error field to the error message.
    이 방법은 양식 제출을 시뮬레이션합니다. 오류가 발생하면 error 필드를 오류 메시지로 설정합니다.

3. resetError Method:

  • This method sets the error field to null. This method is called when the alert is dismissed.
    이 방법은 error 필드를 null로 설정합니다. 이 메서드는 경고가 해제될 때 호출됩니다.

4. v-alert Component:

  • The v-alert component is shown conditionally with v-if="error".
    v-alert 구성요소는 v-if="error"와 함께 조건부로 표시됩니다.
  • It is of type error and dismissible.
    'error' 유형이며 무시할 수 있습니다.
  • The @input event listener is used to call resetError when the alert is dismissed.
    @input 이벤트 리스너는 경고가 해제될 때 resetError를 호출하는 데 사용됩니다.

By following this approach, the error message will be cleared when the user dismisses the alert, ensuring that the UI is updated accordingly.

이 접근 방식을 따르면 사용자가 경고를 닫을 때 오류 메시지가 지워지고 이에 따라 UI가 업데이트됩니다.

알림 메시지 창의 여백

ChatGPT에게 물었습니다.

When displaying a v-alert message, I want to give a margin with style.

v-alert 메시지 출력할 때 style로 margin을 주고 싶어

To add a margin or any other styles to the v-alert component in Vuetify, you can use the class or style attributes. Vuetify allows you to apply custom styles directly to its components.

Vuetify의 v-alert 구성 요소에 여백이나 기타 스타일을 추가하려면 class 또는 style 속성을 사용할 수 있습니다. Vuetify를 사용하면 사용자 정의 스타일을 구성 요소에 직접 적용할 수 있습니다.

Example

Here's how you can add a margin to the v-alert component using the class or style attributes:

class 또는 style 속성을 사용하여 v-alert 구성 요소에 여백을 추가하는 방법은 다음과 같습니다.

Using class

You can define a CSS class with the desired margin and apply it to the v-alert.
원하는 여백으로 CSS 클래스를 정의하고 이를 v-alert에 적용할 수 있습니다.

<template>
  <v-container>
    <v-form @submit.prevent="submitForm">
      <v-text-field v-model="email" label="Email" required></v-text-field>
      <v-text-field v-model="password" label="Password" type="password" required></v-text-field>
      <v-btn type="submit">Submit</v-btn>
    </v-form>
    <v-alert
      v-if="error"
      type="error"
      dismissible
      @input="resetError"
      v-html="error"
      class="my-alert"
    ></v-alert>
  </v-container>
</template>

<script>
export default {
  data() {
    return {
      email: '',
      password: '',
      error: null
    };
  },
  methods: {
    async submitForm() {
      try {
        // Simulate form submission and error
        throw new Error('An error occurred! <br> Please try again.');
      } catch (e) {
        this.error = e.message;
      }
    },
    resetError() {
      this.error = null;
    }
  }
};
</script>

<style scoped>
.my-alert {
  margin: 20px 0;
}
</style>

Using style

You can also apply inline styles directly to the v-alert.
v-alert에 인라인 스타일을 직접 적용할 수도 있습니다.

<template>
  <v-container>
    <v-form @submit.prevent="submitForm">
      <v-text-field v-model="email" label="Email" required></v-text-field>
      <v-text-field v-model="password" label="Password" type="password" required></v-text-field>
      <v-btn type="submit">Submit</v-btn>
    </v-form>
    <v-alert
      v-if="error"
      type="error"
      dismissible
      @input="resetError"
      v-html="error"
      :style="{ margin: '20px 0' }"
    ></v-alert>
  </v-container>
</template>

<script>
export default {
  data() {
    return {
      email: '',
      password: '',
      error: null
    };
  },
  methods: {
    async submitForm() {
      try {
        // Simulate form submission and error
        throw new Error('An error occurred! <br> Please try again.');
      } catch (e) {
        this.error = e.message;
      }
    },
    resetError() {
      this.error = null;
    }
  }
};
</script>

<style scoped>
/* You can add any other component-specific styles here */
</style>

Explanation 설명

1. Using class Attribute:

  • Define a CSS class (my-alert) in the <style scoped> section.
    <style ranged> 섹션에서 CSS 클래스(my-alert)를 정의합니다.
  • Apply the class to the v-alert component using the class attribute.
    class 속성을 사용하여 v-alert 구성요소에 클래스를 적용합니다.

2. Using style Attribute:

  • Apply inline styles directly to the v-alert component using the style attribute with the :style binding.
    :style 바인딩과 함께 style 속성을 사용하여 v-alert 구성 요소에 인라인 스타일을 직접 적용합니다.
  • Both methods will add a margin to the v-alert component, giving it spacing from other elements. Choose the method that best fits your styling approach. The class attribute is preferred for reusable styles, while style is convenient for quick, one-off styles.
    두 방법 모두 v-alert 구성요소에 여백을 추가하여 다른 요소와의 간격을 제공합니다. 귀하의 스타일링 접근 방식에 가장 적합한 방법을 선택하십시오. class 속성은 재사용 가능한 스타일에 선호되는 반면 style은 빠른 일회성 스타일에 편리합니다.

Source Code

1. 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'
import { auth } from '@/firebase';

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;

    dispatch('fetchUsers');

    auth.onAuthStateChanged(user => {
      // 우리는 우리의 웹앱에 저장된 계정 정보를 사용한다.
      // user.uid로 웹앱의 firestore DB에서 계정 정보를 가져온다. 
      if(user != null) {
        dispatch('fetchUserWithUid', {uid: user.uid});
      }
    });
  }
}).$mount('#app')

2. src/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue';
import Login from '@/views/Login.vue';
import Register from '@/views/Register.vue';
import UserInfo from '@/views/UserInfo.vue';
import About from '@/views/About.vue';
import store from '../store';

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  },
  {
    path: '/register',
    name: 'Register',
    component: Register
  },
  {
    path: '/userInfo',
    name: 'UserInfo',
    component: UserInfo,
    meta: { requiresAuth: true }
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
];

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
});

router.beforeEach((to, from, next) => {
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
  const isAuthenticated = store.getters.isAuthenticated;
  if (requiresAuth && !isAuthenticated) {
    next('/login');
  } else {
    next();
  }
});

export default router;

3. src/store/index.js

// src/store/index.js

import Vue from 'vue';
import Vuex from 'vuex';
import { db, firebase, auth, googleProvider } from '@/firebase';
import router from '@/router';  // Vue Router import

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    user: null,
    users: [],
    isLoading: false,
    error: null
  },
  mutations: {
    setUser(state, user) {
      state.user = user;
    },
    setUsers(state, users) {
      state.users = users;
      state.isLoading = false;
    },
    setError(state, error) {
      console.log(error);
      state.error = error;
      state.isLoading = false;
    },
    setLoading(state, isLoading) {
      state.isLoading = isLoading;
    }
  },
  actions: {
    async fetchUsers({ commit }) {
      commit('setLoading', true);
      try {
        const usersSnapshot = await db.collection('users').get();
        const users = usersSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
        console.log(users);
        commit('setUsers', users);
      } catch (error) {
        commit('setError', error.message);
      }
    },
    async addUser({ dispatch, commit }, user) {
      try {
        await db.collection('users').add(user)
        .then(newUser => {

          // 로그인 설정
          user.id = newUser.id;
          console.log('addUser :', user);
          commit('setUser', user);
        });

        dispatch('fetchUsers');
      } catch (error) {
        console.error('Error adding user:', error);
      }
    },
    async deleteUser({ dispatch }, userId) {
      try {
        await db.collection('users').doc(userId).delete();
        dispatch('fetchUsers');
      } catch (error) {
        console.error('Error deleting user:', error);
      }
    },
    async updateUser({ dispatch }, { userId, user }) {
      try {
        await db.collection('users').doc(userId).update(user);
        dispatch('fetchUsers');
      } catch (error) {
        console.error('Error updating user:', error);
      }
    },
    async login({ commit, dispatch }, { email, password }) {
      try {
        const { user } = await auth.signInWithEmailAndPassword(email, password);;
        // 웹앱의 계정 정보를 가져와 로그인 설정을 한다.
        dispatch('fetchUserWithUid', {uid: user.uid});       
        router.push("/");   // home으로
      } catch (error) {
        commit('setError', error.message);
      }
    },
    async fetchUserWithUid({ commit }, {uid}) {
      //console.log(uid);
      try {
        const querySnapshot = await db.collection('users').where('uids', 'array-contains', uid).get();
        //const user = querySnapshot.docs.map(doc => doc.data());
        const user = querySnapshot.docs. map(doc => ({ id: doc.id, ...doc.data() }));
        //console.log('Matching user:', user[0]);
        // 로그인 설정을 한다.
        commit('setUser', user[0]);
      } catch (error) {
        console.error('Error fetching user:', error);
      }            
    },
    async register({ commit, dispatch }, { email, password, name }) {
      try {
        const { user } = await auth.createUserWithEmailAndPassword(email, password, name);
        // 웹앱에 계정을 만든다.
        const newUser = {
          email: email,
          name: name,
          uids: [user.uid]
        };
        // newUser 객체가 추가됨
        // dispatch('addUser', { user: newUser });
        // newUser 계정 정보가 추가됨
        dispatch('addUser', newUser);

        router.push("/");   // home으로        
      } catch (error) {
        console.log(error);
        commit('setError', error.message);
      }
    },
    async addUidToUser({ state }, newUid) {      
      //console.log('user.id:', state.user.id);
      if (state.user) {
        try {
          await db.collection('users').doc(state.user.id).update({
            uids: firebase.firestore.FieldValue.arrayUnion(newUid)
          });
          // Update the local state if neededa
          state.user.uids = [...(state.user.uids || []), newUid];
        } catch (error) {
          console.error('Error adding UID to user:', error);
        }
      }
    },
    async addGoogleUid({ commit, dispatch, getters }) {
      try {
        const { user } = await auth.signInWithPopup(googleProvider);
        try {
          // 이미 연동된 회원의 경우 알림 메시지 출력한다.
          const myUser = getters.getUserByUid(user.uid);
          if (myUser) {
            console.log("sos:", myUser);
            // 이미 연동되어 있다.
            commit('setError','이미 연동되어 있습니다.');
          } else {
            // 구글 연동을 진행한다.
            dispatch('addUidToUser', user.uid);
          }          
        } catch (error) {
          commit('setError','Error adding google uid');
        }
      } catch (error) {
        commit('setError', error.message);
      }
    },
    async googleLogin({ commit, getters }) {
      try {
        // 구글 계정에 로그인
        const { user } = await auth.signInWithPopup(googleProvider);
        try {
          // 구글계정의 uid로 웹앱 계정의 정보를 가져옴.
          // 웹앱 계정은 사이트에 접속할 때 전체 회원 정보를 로드하였으므로 
          // 이미 로드된 회원 리스트에서 구글 계정 uid를 가진 myUser를 가져온다.
          const myUser = getters.getUserByUid(user.uid);
          if (myUser) {
            commit('setUser', myUser);
            router.push("/");   // home으로
          } else {
            console.log('등록된 회원이 아닙니다.');
            // 이경우 회원 가입 페이지로 이동 필요
          }
        } catch (error) {
          console.error('Error adding user:', error);
        }
      } catch (error) {
        commit('setError', error.message);
      }
    },
    async logout({ commit }) {
      await auth.signOut();
      commit('setUser', null);
    },
    resetError({ commit }) {
      commit('setError', null);
    },
    setUser({ commit }, user) {
      commit('setUser', user);
    }
  },
  getters: {
    user: state => state.user,
    isAuthenticated: state => !!state.user,
    users: state => state.users,
    isLoading: state => state.isLoading,
    error: state => state.error,
    getUserByUid: (state) => (uid) => {
      return state.users.find(user => user.uids && user.uids.includes(uid));
    }
  }
});

4. src/views/Login.vue

<!-- src/views/Login.vue -->
<template>
  <v-container>
    <v-row>
      <v-col cols="12" class="text-center my-5">
        <h3>로그인</h3>
      </v-col>      
    </v-row>
    <v-row>
      <v-col class="text-center" cols="10" offset="1" sm="8" offset-sm="2">   
        <v-form @submit.prevent="loginUser">
          <v-text-field v-model="email" label="이메일" type="email" required></v-text-field>
          <v-text-field v-model="password" label="비밀번호" type="password" required></v-text-field>
          <v-btn type="submit" color="primary">Login</v-btn>
          <v-alert v-if="error" type="error" dismissible>{{ error }}</v-alert>
        </v-form>
      </v-col>
    </v-row>
    <v-row>
      <v-col class="text-center" cols="10" offset="1" sm="8" offset-sm="2">  
        <v-btn color="red" @click="googleLogin" dark>
          <v-icon left>mdi-google</v-icon>
          Sign in with Google
        </v-btn>
      </v-col>
    </v-row>
  </v-container>
</template>

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

export default {
  name: 'Login',
  data() {
    return {
      email: '',
      password: ''
    };
  },
  computed: {
    ...mapGetters(['error'])
  },
  methods: {
    ...mapActions(['login']),
    ...mapActions(['googleLogin']),
    async loginUser() {
      await this.login({ email: this.email, password: this.password });
    }
  }
};
</script>

5. src/views/Register.vue

<!-- src/views/Register.vue -->
<template>
  <v-container>
    <v-row>
      <v-col cols="12" class="text-center my-5">
        <h3>계정 만들기</h3>
      </v-col>      
    </v-row>
    <v-row>
      <v-col class="text-center" cols="10" offset="1" sm="8" offset-sm="2">    
        <v-form @submit.prevent="userRegister">
          <v-text-field v-model="email" label="이메일" type="email" required></v-text-field>
          <v-text-field v-model="name" label="이름" type="text" required></v-text-field>
          <v-text-field v-model="password" label="비밀번호" type="password" required></v-text-field>
          <v-text-field v-model="confirmPassword" name="confirmPassword" label="비밀번호 확인" type="password" required :rules="[comparePassword]"></v-text-field>
          <v-progress-circular v-if="isLoading" indeterminate :width="7" :size="70" color="grey lighten-1"></v-progress-circular>    
          <v-btn type="submit" color="primary">Register</v-btn>
          <v-alert v-if="error" type="error" dismissible>{{ error }}</v-alert>
        </v-form>
      </v-col>
    </v-row>
  </v-container>
</template>

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

export default {
  data() {
    return {
      email: '',
      name: '',
      password: '',
      confirmPassword: ''
    };
  },
  computed: {
    ...mapGetters(['error', 'isLoading']),
    comparePassword() {
      if(this.password == this.confirmPassword) return true;
      else return "비밀번호가 일치하지 않습니다.";
    },
  },
  methods: {
    ...mapActions(['register']),
    async userRegister() {
      await this.register({ email: this.email, password: this.password, name: this.name });
    }
  }
};
</script>

6. src/views/UserInfo.vue

<!-- src/views/UserInfo.vue -->
<template>
  <v-container>
    <v-card v-if="user">
      <v-card-title>User Information</v-card-title>
      <v-card-text>
        <p><strong>이메일:</strong> {{ user.email }}</p>
        <p><strong>이름:</strong> {{ user.name }}</p>
        <p><strong>id:</strong> {{ user.id }}</p>
      </v-card-text>
      <v-card-text>
        <v-btn color="red" @click="addGoogleAccount" dark>  
          <v-icon left>mdi-google</v-icon>
          Google 계정 연동
        </v-btn>
        <v-alert v-if="error" type="error" dismissible @input="resetErrorMsg" class="my-alert">{{ error }}</v-alert>
      </v-card-text>
    </v-card>
    <v-alert v-else type="info">No user is logged in.</v-alert>
  </v-container>
</template>

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

export default {
  computed: {
    ...mapGetters(['user', 'error'])
  },
  methods: {
    ...mapActions(['addGoogleUid', 'resetError']),
    async addGoogleAccount() {
      await this.addGoogleUid();
    },
    resetErrorMsg() {
      this.resetError();
    }
  }
};
</script>

<style scoped>
.my-alert {
  margin: 20px 0;
}
</style>

7. App.vue

Copy<template>
  <v-app>
    <v-navigation-drawer app>
      <v-list>
        <v-list-item link>
          <router-link to="/">Home</router-link>
        </v-list-item>
        <v-list-item link  v-if="!user">
          <router-link to="/login" >로그인</router-link>
        </v-list-item>
        <v-list-item link>
          <router-link to="/register">계정 만들기</router-link>
        </v-list-item>
        <v-list-item link v-if="user">
          <router-link to="/userInfo">계정 정보</router-link>
        </v-list-item>
        <v-list-item link>
          <router-link to="/about">About</router-link>
        </v-list-item>
      </v-list>
    </v-navigation-drawer>

    <v-app-bar app>
      <v-toolbar-title>My Auth App</v-toolbar-title>
      <v-spacer></v-spacer>
      <v-btn icon @click="doLogout" v-if="user">
        <v-icon>mdi-logout</v-icon>
      </v-btn>
    </v-app-bar>

    <v-main>
      <router-view></router-view>
    </v-main>
  </v-app>
</template>

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

export default {
  name: 'App',
  computed: {
    ...mapGetters(['user'])
  },
  methods: {
    ...mapActions(['logout']),
    doLogout() {
      this.logout()
    }
  }
};
</script>