회원가입 시 들어가야 할 항목은 다음과 같다.
- 이름
- 아이디
- 비밀번호
- 비밀번호 확인
- 생년월일
- 주소
- 전화번호
- 이메일 주소
그리고 디테일한 요구사항은 다음과 같다.
- 회원가입 / 로그인 창은 심플하게 구현한다.
- 비밀번호 및 비밀번호 확인은 암호화 하도록 한다.
- 비밀번호 및 비밀번호 확인란에서 capslock이 눌려 있을 시 고객에게 알려줘야 한다.
- 주소 입력은 카카오(다음) 우편번호 서비스 api를 이용해 구현한다.
근데 여기에 아이디, 이메일의 경우 중복 체크, 유효성 검사도 넣어야 할 것 같다.
0221-0222
이틀간 한 일 :
팀 회의
계획 및 스프린트 양식 미리 작성
팀 프로젝트를 위한 github repository 생성 및 콜라보레이터 추가
vue(+ bootstrap vue)로 먼저 폼 작성
주소 입력 부분은 다음 우편번호 서비스 api를 이용해 추가
<template>
<div style="padding-top: 5%">
<div>
<b-row align-h="center">
<b-col cols="3">
<b-card title="회원가입">
<b-form-group>
<label for="userId">아이디</label>
<b-form-input
id="userId"
v-model="userId"
:state="userIdState"
aria-describedby="아이디"
required
size="sm"
trim
></b-form-input>
<!-- 조건 미충족 시 -->
<b-form-invalid-feedback id="input-live-feedback">
아이디는 5-12글자 사이여야 합니다.
</b-form-invalid-feedback>
<!-- 조건 충족 시 -->
<b-form-text id="input-live-help"></b-form-text>
</b-form-group>
<b-form-group>
<label for="userPw">비밀번호</label>
<b-form-input
id="userPw"
v-model="userPw"
:state="userPwState"
aria-describedby="비밀번호"
type="password"
size="sm"
required
trim
></b-form-input>
<!-- 조건 미충족 시 -->
<b-form-invalid-feedback id="input-live-feedback">
비밀번호는 8-20글자 사이여야 합니다.
</b-form-invalid-feedback>
<!-- 조건 충족 시 -->
<b-form-text id="input-live-help"></b-form-text>
</b-form-group>
<b-form-group>
<label for="userPw">비밀번호 재확인</label>
<b-form-input
id="userPwCheck"
v-model="userPwCheck"
:state="userPwCheckState"
aria-describedby="비밀번호 재확인"
type="password"
size="sm"
required
trim
></b-form-input>
<!-- 조건 미충족 시 -->
<b-form-invalid-feedback id="input-live-feedback">
비밀번호가 일치하지 않습니다.
</b-form-invalid-feedback>
<!-- 조건 충족 시 -->
<b-form-text id="input-live-help"></b-form-text>
</b-form-group>
<b-form-group>
<label for="userName">이름</label>
<b-form-input
id="userName"
v-model="userName"
:state="userNameState"
aria-describedby="이름"
size="sm"
required
trim
></b-form-input>
<!-- 조건 미충족 시 -->
<b-form-invalid-feedback id="input-live-feedback">
최소 2글자 이상 입력해 주세요
</b-form-invalid-feedback>
<!-- 조건 충족 시 -->
<b-form-text id="input-live-help"></b-form-text>
</b-form-group>
<b-form-group>
<label for="userEmail">이메일</label>
<b-form-input
id="userEmail"
v-model="userEmail"
aria-describedby="이메일"
type="email"
placeholder="example@example.com"
size="sm"
required
trim
></b-form-input>
</b-form-group>
<b-form-group>
<label for="userPhoneNumber">전화번호</label>
<b-form-input
id="userPhoneNumber"
v-model="userPhoneNumber"
aria-describedby="전화번호"
placeholder="000-0000-0000"
type="text"
size="sm"
:formatter="formatNumber"
required
trim
></b-form-input>
</b-form-group>
<b-form-group>
<label for="userEmail">생년월일</label>
<b-form-input
id="userBirth"
v-model="userBirth"
aria-describedby="생년월일"
type="date"
size="sm"
required
trim
></b-form-input>
</b-form-group>
<b-form-group class="daummap">
<b-row align-h="center">
<label for="userAddress">주소</label>
<b-col cols="5">
<b-form-input
id="userZip"
v-model="userZip"
type="text"
placeholder="우편번호"
size="sm"
></b-form-input>
</b-col>
<b-col> <b-button size="sm" @click="showApi">우편번호 찾기</b-button></b-col>
<b-form-input
id="userAddr1"
v-model="userAddr1"
type="text"
placeholder="주소"
size="sm"
></b-form-input>
<b-form-input
id="userAddr2"
v-model="userAddr2"
type="text"
placeholder="상세주소"
size="sm"
></b-form-input>
</b-row>
</b-form-group>
</b-card>
</b-col>
</b-row>
</div>
</div>
</template>
<script>
export default {
name: 'Daummap',
data() {
return {
userId: '',
userPw: '',
userPwCheck: '',
userName: '',
userEmail: '',
userPhoneNumber: '',
userBirth: '',
userZip: '',
userAddr1: '',
userAddr2: ''
}
},
computed: {
userIdState() {
return this.userId.length > 4 && this.userId.length < 13
},
userPwState() {
return this.userPw.length > 7 && this.userPw.length < 21
},
userPwCheckState() {
return this.userPwCheck === this.userPw
},
userNameState() {
return this.userName.length > 1 ? true : false
}
},
methods: {
// 전화번호 글자수 제한
formatNumber(e) {
return String(e).substring(0, 13)
},
// 우편번호 찾기
showApi() {
new window.daum.Postcode({
oncomplete: data => {
// 팝업에서 검색결과 항목을 클릭했을 때 실행할 코드를 작성하는 부분
// 도로명 주소의 노출 규칙에 따라 주소를 조합한다.
// 내려오는 변수가 값이 없는 경우엔 공백('') 값을 가지므로, 이를 참고하여 분기 한다.
let fullRoadAddr = data.roadAddress // 도로명 주소 변수
let extraRoadAddr = '' // 도로명 조합형 주소 변수
// 법정동명이 있을 경우 추가한다. (법정리는 제외)
// 법정동의 경우 마지막 문자가 "동/로/가"로 끝난다.
if (data.bname !== '' && /[동|로|가]$/g.test(data.bname)) {
extraRoadAddr += data.bname
}
// 건물명이 있고, 공동주택일 경우 추가한다.
if (data.buildingName !== '' && data.apartment === 'Y') {
extraRoadAddr += extraRoadAddr !== '' ? ', ' + data.buildingName : data.buildingName
}
// 도로명, 지번 조합형 주소가 있을 경우, 괄호까지 추가한 최종 문자열을 만든다.
if (extraRoadAddr !== '') {
extraRoadAddr = ' (' + extraRoadAddr + ')'
}
// 도로명, 지번 주소의 유무에 따라 해당 조합형 주소를 추가한다.
if (fullRoadAddr !== '') {
fullRoadAddr += extraRoadAddr
}
// 우편번호와 주소 정보를 해당 필드에 넣는다.
this.userZip = data.zonecode // 5자리 새 우편번호 사용
this.userAddr1 = fullRoadAddr
}
}).open()
}
}
}
</script>
결과
'우편번호 찾기'는 웹 서비스라 임베디드가 아닌 팝업으로 구현했다.
참고한 링크
기록
정말 기본적이고 임시적인 코드인데 하루 좀 넘게 걸렸다.
이렇게 오래 걸린 가장 큰 이유는 생년월일 칸을 부트스트랩으로 네이버 회원가입처럼 만드려다가...(select가 말을 듣지 않음) 정말 내가 아무것도 모른다는 사실만 깨달았다. (결국 팀원분의 충고에 따라 달력 입력 형식(date)으로 변경했다.)
이제 후딱 해야겠다는 생각도 잠시, 다음 api 사용하려고 공식 사이트의 js 코드만 눈 빠져라 보다가 vue 키워드 넣어서 검색하고 단번에 해냈다.
혼자 고집 부리지 말고 구글링을 잘 하자...
남은 할 일
- 비밀번호 capslock 눌려있으면 알림 기능 구현
- 비밀번호 숫자, 문자, 특수문자 포함 기능 구현
- 아이디 유효성 체크(시간 되면 중복 검사)
- 이메일 유효성 체크
- 백엔드 api 연동
- 코드 및 디자인 다듬기(마지막)
0223
- 비밀번호 숫자, 문자, 특수문자 포함 기능 구현
- 이름/아이디/이메일 유효성 체크
- 전화번호 파이프 자동 입력(수정 필요)
- 로그인 화면만 구현
- 라우터 정리
- NotFound.vue(NotFound 페이지 추가)
- 팀원분 파일과 git merge
signup.vue
// ...
computed: {
userIdState() {
return this.userId.length > 4 && this.userId.length < 13 && /^[a-zA-Z0-9]*$/.test(this.userId)
},
userPwState() {
return (
this.userPw.length > 7 &&
this.userPw.length < 21 &&
/^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]*$/.test(this.userPw)
) // 영문 대문자+소문자+특수문자 8-20자리
},
userPwCheckState() {
return this.userPwCheck === this.userPw
},
userNameState() {
return this.userName.length > 1 && /^[가-힣]*$/.test(this.userName) // 한글 2자리 이상
},
userEmailState() {
return (
this.userEmail.length > 5 &&
/^[A-Za-z0-9_]+[A-Za-z0-9]*[@]{1}[A-Za-z0-9]+[A-Za-z0-9]*[.]{1}[A-Za-z]{1,3}$/.test(this.userEmail)
) // 이메일 형식(영문대소문자/숫자+@+영문대소문자/숫자+.+영문대소문자 3자리)
}
},
methods: {
// 전화번호 글자수 제한
formatNumber(e) {
return String(e).substring(0, 13) // 최대 11자리 010-1234-5678
},
// 전화번호 숫자만 입력 시 파이프(-) 자동 입력
getPhoneMask(val) {
let res = this.getMask(val)
this.userPhoneNumber = res
// 서버 전송 값에는 '-'를 제외하고 숫자만 저장
this.model.userPhoneNumber = this.userPhoneNumber.replace(/[^0-9]/g, '')
},
getMask(inputNumber) {
if (!inputNumber) return inputNumber
inputNumber = inputNumber.replace(/[^0-9]/g, '')
let res = ''
if (inputNumber.length < 3) {
res = inputNumber
} else {
if (inputNumber.substr(0, 2) == '02') {
if (inputNumber.length <= 5) {
//02-123까지만 입력 되어도 - 삽입
res = inputNumber.substr(0, 2) + '-' + inputNumber.substr(2, 3)
} else if (inputNumber.length > 5 && inputNumber.length <= 9) {
//02-123-4567
res = inputNumber.substr(0, 2) + '-' + inputNumber.substr(2, 3) + '-' + inputNumber.substr(5)
} else if (inputNumber.length > 9) {
//02-1234-5678
res = inputNumber.substr(0, 2) + '-' + inputNumber.substr(2, 4) + '-' + inputNumber.substr(6)
}
} else {
// 010-1234-5678
if (inputNumber.length < 8) {
res = inputNumber
} else if (inputNumber.length == 8) {
res = inputNumber.substr(0, 4) + '-' + inputNumber.substr(4)
} else if (inputNumber.length == 9) {
res = inputNumber.substr(0, 3) + '-' + inputNumber.substr(3, 3) + '-' + inputNumber.substr(6)
} else if (inputNumber.length == 10) {
res = inputNumber.substr(0, 3) + '-' + inputNumber.substr(3, 3) + '-' + inputNumber.substr(6)
} else if (inputNumber.length > 10) {
res = inputNumber.substr(0, 3) + '-' + inputNumber.substr(3, 4) + '-' + inputNumber.substr(7)
}
}
}
return res
},
// ...
// 콘솔 로그 확인
onSubmit() {
console.log(
'onSubmit',
this.userId,
this.userPw,
this.userPwCheck,
this.userPwCheck,
this.userName,
this.userEmail,
this.userPhoneNumber,
this.userBirth,
this.userZip,
this.userAddr1,
this.userAddr2
)
}
// ...
views/auth/login.vue
<template>
<div>
<div>
<b-row align-h="center">
<b-col cols="4">
<b-card title="로그인" style="margin-top: 25vh">
<b-form-group label-cols="4" label-cols-lg="3" label="아이디" label-for="input-userid">
<b-form-input id="input-userid" v-model="userLoginId"></b-form-input>
</b-form-group>
<b-form-group label-cols="4" label-cols-lg="3" label="비밀번호" label-for="input-password">
<b-form-input id="input-password" v-model="userLoginPw" type="password"></b-form-input>
</b-form-group>
<b-form-group label-cols="4" label-cols-lg="3" label="">
<b-button variant="primary" @click="onSubmit">로그인</b-button>
</b-form-group>
</b-card>
</b-col>
</b-row>
</div>
</div>
</template>
<script>
export default {
data() {
return {
userLoginId: null,
userLoginPw: null
}
},
methods: {
onSubmit() {
console.log('onSubmit', this.userLoginId, this.userLoginPw)
}
}
}
</script>
<style lang="scss" scoped></style>
router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Main from '../views/main/Main.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
component: Main
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
},
{
path: '/main',
component: Main
},
{
path: '/reservation',
component: () => import('../views/reservation/Reservation.vue')
},
{
path: '/reservation-Check',
component: () => import('../views/reservation/Reservation_check.vue')
},
{
path: '/my-page',
component: () => import('../views/my_page/My_page.vue')
},
{
path: '/auth',
component: () => import('../views/auth'),
children: [
{
path: '/auth/login',
component: () => import('../views/auth/login')
},
{
path: '/auth/sign',
component: () => import('../views/auth/signup')
}
]
},
// NotFound 페이지 (항상 맨 밑에)
{
path: '*',
component: () => import('../components/NotFound.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
결과
참고한 링크
기록
파이프 제외하고 데이터 입력하는 거...연구해봐야겠다.
회원가입 꼭 해내고 싶다ㅎ
0224
- 회원가입 백엔드 api 연동을 위한 user.js 스토어 작성
- 백엔드, DB 연동
views/auth/signup.vue
// ...
<script>
export default {
// name: 'Daummap',
data() {
return {
user: {
userId: '',
userPw: '',
userPwCheck: '',
userName: '',
userEmail: '',
userPhoneNumber: '',
userBirth: '',
userZip: '',
userAddr1: '',
userAddr2: ''
}
}
},
computed: {
infoData() {
return this.$store.getters.User
},
userIdState() {
return this.user.userId.length > 4 && this.user.userId.length < 13 && /^[a-zA-Z0-9]*$/.test(this.user.userId)
},
userPwState() {
return (
this.user.userPw.length > 7 &&
this.user.userPw.length < 21 &&
/^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]*$/.test(this.user.userPw)
) // 영문 대문자+소문자+특수문자 8-20자리
},
userPwCheckState() {
return this.user.userPwCheck === this.user.userPw
},
userNameState() {
return this.user.userName.length > 1 && /^[가-힣]*$/.test(this.user.userName) // 한글 2자리 이상
},
userEmailState() {
return (
this.user.userEmail.length > 5 &&
/^[A-Za-z0-9_]+[A-Za-z0-9]*[@]{1}[A-Za-z0-9]+[A-Za-z0-9]*[.]{1}[A-Za-z]{1,3}$/.test(this.user.userEmail)
) // 이메일 형식(영문대소문자/숫자+@+영문대소문자/숫자+.+영문대소문자 3자리)
},
insertedResult() {
return this.$store.getters.UserInsertedResult
}
},
watch: {
infoData(value) {
console.log('watch.infoData', value)
},
insertedResult(value) {
console.log('watch.insertedResult', value)
// 등록 후 처리
if (value !== null) {
if (value > 0) {
// 등록이 성공한 경우
// 회원가입 완료 시 메인 페이지로 이동
this.$router.replace('/main')
} else {
// 회원가입 실패한 경우 토스트 메세지 출력
this.$bvToast.toast('등록이 실패하였습니다.', {
title: 'ERROR',
variant: 'danger',
solid: true
})
// this.$router.go() // 현재 페이지 새로고침
}
}
}
},
// ...
},
// 콘솔 로그 확인
onSubmit() {
// console.log('onSubmit', { ...this.user })
// 초기화
this.$store.dispatch('actUserInit') // null값으로 초기화
// 등록
this.$store.dispatch('actUserInsert', this.user)
}
}
}
</script>
store/models/user.js
import api from '../apiUtil'
// 초기값 선언
const stateInit = {
User: {
userId: null,
userPw: null,
userPwCheck: null,
userName: null,
userEmail: null,
userPhoneNumber: null,
userBirth: null,
userZip: null,
userAddr1: null,
userAddr2: null
}
}
export default {
state: {
// state에 사용할 모델이나 값을 선언 한다.
User: { ...stateInit.User },
InsertedResult: null
},
getters: {
// getters을 통해 state값을 호출 한다.
User: state => state.User,
UserInsertedResult: state => state.InsertedResult
},
mutations: {
// mutations는 동기적이어야 함.(비동기 사용 불가)
setUser(state, data) {
state.User = data
},
setInsertedResult(state, data) {
state.InsertedResult = data
}
},
actions: {
// action은 비동기적 사용이 가능하다. (action에서 mutation을 호출하는 방법을 권장함)
// 등록
actUserInsert(context, payload) {
console.log('actUserInsert', payload) // 적은 정보가 payload를 통해 넘어옴
// 상태(결과)값 초기화
context.commit('setInsertedResult', null)
// // 백엔드 호출 (결과값 수신)(watch에서 감지하게 해주기 위해 setTimeout 사용)
// setTimeout(() => {
// const insertedResult = 0
// context.commit('setInsertedResult', insertedResult)
// }, 300) // state값의 변화를 감지하기 위하여 일부러 지연 시켰다.
/* RestAPI 호출 */
api
.post('/serverApi/register', payload)
.then(response => {
const insertedResult = response && response.data && response.data.id
context.commit('setInsertedResult', insertedResult)
})
.catch(error => {
// 에러인 경우 처리
console.error('UserInsert.error', error)
context.commit('setInsertedResult', -1)
})
},
// 초기화
actUserInit(context, payload) {
context.commit('setUser', { ...stateInit.User }) // 현재 값만 들어감/setUser에 statInit.User 요소를 풀어서 넣어줌=전부 복붙(null값이 됨)
// context.commit('setUser', stateInit.User) // 메모리 주소 복사(User가 변경되면 변경된 값이 들어감)
}
}
}
결과
에러 참고
store/index.js
모듈 import를 여기서 안해줬더니 에러가 엄청 떴다...구글링 해도 해결방법을 못찾아서 진짜 암담 했었다...
다른 파일도 살펴보자...
에러 내용 : Vuex error unknown action type
import Vue from 'vue'
import Vuex from 'vuex'
import User from './models/user'
Vue.use(Vuex)
export default new Vuex.Store({
state: {},
mutations: {},
actions: {},
modules: {
User
}
})
참고한 링크
Git push가 안되는 경우(fatal:refusing to merge unrelated histories)
[Git] Please commit your changes or stash them before you merge
0225
- 로그인 백엔드 api 연동을 위한 user.js 스토어 작성
- 백엔드, DB 연동
models/auth.js
import api from '../apiUtil'
const stateInit = {
UserLogin: {
userId: null,
password: null
}
}
export default {
state: {
// state에 사용할 모델이나 값을 선언 한다.
UserLogin: { ...stateInit.UserLogin }
},
getters: {
// getters을 통해 state값을 호출 한다.
UserLogin: state => state.UserLogin
},
mutations: {
// 동기적
setUserLogin(state, data) {
state.UserLogin = data
}
},
actions: {
// 비동기 (mutations 호출)
// login
actUserLogin(context, payload) {
console.log('actUserLogin', payload) // 정보가 payload를 통해 넘어옴
// 상태(결과)값 초기화
context.commit('setUserLogin', null)
/* RestAPI 호출 */
api
.post('/serverApi/login', payload)
.then(response => {
console.log('response', response)
const UserLogin = response
context.commit('setUserLogin', UserLogin)
})
.catch(error => {
// 에러인 경우 처리
console.error('setUserLogin.error', error)
context.commit('setUserLogin', -1)
})
}
}
}
0228
- 회원가입 폼(로직) 완성
- 로그인 폼 완성
회원가입
views/signup.vue
1. 입력값이 null이거나 유효성 체크에서 조건을 충족하지 못한 경우 회원가입 버튼을 누를 때(onSubmit) 유저에게 alert로 알려주고 값이 백엔드로 넘어가는 것을 막도록 코드를 짰다.
// ...
<b-form-group>
<label for="userId">아이디</label>
<b-form-input
id="userId"
ref="userId" // focus()를 위해 추가(폼에 커서가 자동으로 간다)
v-model="user.userId"
:state="userIdState"
aria-describedby="아이디"
required
size="sm"
trim
></b-form-input>
<!-- 조건 미충족 시 --> // v-if 추가(값이 입력되어 null이 아닐때만 경고 메세지 표시)
<b-form-invalid-feedback v-if="user.userId" id="input-live-feedback">
아이디는 영문 대소문자와 <br />
숫자 5-12자리로 입력해야 합니다
</b-form-invalid-feedback>
<!-- 조건 충족 시 -->
<b-form-text v-if="user.userId" id="input-live-help"></b-form-text>
</b-form-group>
// ...
methods: {
// 공란 및 유효성 여부 체크
checkInput() {
const inputForm = this.user
if (inputForm.userId == '') {
alert('아이디를 입력해 주세요')
this.$refs.userId.focus()
return false
} else if (this.userIdState === false) {
this.$refs.userId.focus()
return false
}
if (inputForm.userPw == '') {
alert('비밀번호를 입력해 주세요')
this.$refs.userPw.focus()
return false
} else if (this.userPwState === false) {
this.$refs.userPw.focus()
return false
}
if (inputForm.userPwCheck == '') {
alert('비밀번호를 확인해 주세요')
this.$refs.userPwCheck.focus()
return false
} else if (this.userPwCheckState === false) {
this.$refs.userPwCheck.focus()
return false
}
if (inputForm.userName == '') {
alert('이름을 입력해 주세요')
this.$refs.userName.focus()
return false
} else if (this.userNameState === false) {
this.$refs.userName.focus()
return false
}
if (inputForm.userEmail == '') {
alert('이메일을 입력해 주세요')
this.$refs.userEmail.focus()
return false
} else if (this.userEmailState === false) {
this.$refs.userEmail.focus()
return false
}
if (inputForm.userPhoneNumber == '') {
alert('전화번호를 입력해 주세요')
this.$refs.userPhoneNumber.focus()
return false
} else if (this.formatNumber === false) {
alert('전화번호를 확인해 주세요')
this.$refs.userPhoneNumber.focus()
return false
}
if (inputForm.userBirth == '') {
alert('생년월일을 입력해 주세요')
this.$refs.userBirth.focus()
return false
}
},
// ...
// Sign 버튼 눌렀을 시
onSubmit() {
console.log('onSubmit', { ...this.user })
this.checkInput()
// 초기화
this.$store.dispatch('actUserInit') // null값으로 초기화
// 등록
this.$store.dispatch('actUserInsert', this.user)
// 회원가입 완료 시 로그인 페이지로 이동
this.$router.replace('/auth/login') // 히스토리 기록 안남음
}
}
}
</script>
백엔드 팀원분이 짜주신 코드를 보고 회원가입 성공 여부 데이터를 넘겨받도록 짰다.
models/user.js
// ...
// 등록
actUserInsert(context, payload) {
console.log('actUserInsert', payload) // 적은 정보가 payload를 통해 넘어옴
// 상태(결과)값 초기화
context.commit('setInsertedResult', null)
/* RestAPI 호출 */
api
.post('/serverApi/register', payload)
.then(response => {
console.log('회원가입 response', response)
const insertedResult = response && response.data && response.data.success
context.commit('setInsertedResult', insertedResult)
})
.catch(error => {
// 에러인 경우 처리
console.error('UserInsert.error', error)
context.commit('setInsertedResult', -1)
})
},
// ...
views/signup.vue
넘겨받은 데이터를 프론트에서 받아서 watch로 true/false 결과를 실시간으로 확인할 수 있도록 했다.
true/false 결과에 따라 동작이 달라지도록 if문으로 짰다.
// ...
computed: {
// ...
signResult() {
return this.$store.getters.UserInsertedResult
}
},
watch: {
signResult(value) {
// console.log('watch.signResult', value)
// 회원가입에 성공한 경우
if (value === true) {
alert('회원가입 되었습니다')
this.$router.replace('/auth/login') // 히스토리 기록 안남음
} else if (value === false) {
alert('이미 존재하는 회원입니다')
}
}
},
// ...
결과
0302
- 로그인 토큰 관리 로직 수정
- 입력폼 공란 처리
- 로그인 완료 시 메인 페이지 이동 처리
- 로그인 실패 시 alert 처리
views/auth/login.vue
<template>
<div>
<div>
<b-row align-h="center">
<b-col cols="4">
<b-card title="로그인" style="margin-top: 25vh">
<b-form-group label-cols="4" label-cols-lg="3" label="아이디" label-for="loginId">
<b-form-input id="loginId" ref="loginId" v-model="userLogin.loginId"></b-form-input>
</b-form-group>
<b-form-group label-cols="4" label-cols-lg="3" label="비밀번호" label-for="loginPw">
<b-form-input id="loginPw" ref="loginPw" v-model="userLogin.loginPw" type="password"></b-form-input>
</b-form-group>
<b-form-group label-cols="4" label-cols-lg="3" label="">
<b-button variant="primary" @click="onSubmit">로그인</b-button>
</b-form-group>
</b-card>
</b-col>
</b-row>
</div>
</div>
</template>
<script>
import jwtDecode from 'jwt-decode'
import VueCookies from 'vue-cookies'
import Vue from 'vue'
export default {
data() {
return {
userLogin: {
loginId: null,
loginPw: null
}
}
},
computed: {
tokenUser() {
return this.$store.getters.TokenUser
},
error() {
return this.$store.getters.Error
}
},
watch: {
tokenUser(value) {
console.log('watch.tokenUser', value)
if (value && value.id && value.id !== null) {
// 로그인이 완료된 상황
this.$router.replace('/main') // 메인 페이지 이동
}
},
error(errValue) {
if (errValue !== null) {
// 메세지 출력
alert('아이디, 비밀번호를 확인해 주세요')
}
}
},
created() {
// 이미 토큰을 가지고 있는 경우 처리를 위한 로직
// const token = document.cookie('auth')
const token = VueCookies.get('auth')
console.log('쿠키 토큰 존재', token)
if (token) {
const decodedToken = jwtDecode(token)
console.log('decodedToken.exp', decodedToken.exp)
const today = new Date()
const expDate = new Date(decodedToken.exp * 1000)
if (expDate && expDate >= today) {
// 토큰이 유효한 경우
this.$router.replace('/main') // 메인 페이지로 이동
} else {
// 토큰이 만료된 경우
VueCookies.remove('auth') // 토큰 삭제
}
}
},
methods: {
// 공란 비허용
checkInput() {
if (this.userLogin.loginId == null || this.userLogin.loginId == '') {
alert('아이디를 입력해 주세요')
this.$refs.loginId.focus()
return false
} else if (this.userLogin.loginPw == null || this.userLogin.loginPw == '') {
alert('비밀번호를 입력해 주세요')
this.$refs.loginPw.focus()
return false
}
},
onSubmit() {
this.checkInput()
this.$store.dispatch('actauthLogin', { ...this.userLogin })
}
}
}
</script>
<style lang="scss" scoped></style>
3. VueCookies.get('auth') (= this.$cookies.get('auth'))
에러 참고
쿠키의 토큰 불러오기
현재 created()의 사용자가 이미 로그인을 한 경우 재로그인을 막는 코드를 짜다가 벽에 부딪혔다.
이미 브라우저의 cookie에 존재하는 jwt 토큰을 읽어오질 못한다.
내가 짠 로직은 다음과 같다.
- npm의 vue-cookies의 get을 이용하여 token 변수에 이미 브라우저의 쿠키에 존재하는 jwt 토큰을 불러옴
- 토큰의 만료시간 비교를 위한 today 변수 생성
- 불러온 토큰의 exp(시간) 확인을 위한 expDate 변수 생성(단위 통일을 위해 * 1000)
- 토큰이 유효한 경우 main 페이지로 이동
- 토큰이 만료된 경우 vue-cookies의 remove를 이용해 토큰 삭제
created() {
// 이미 토큰을 가지고 있는 경우 처리를 위한 로직
// const token = document.cookie('auth')
const token = VueCookies.get('auth')
console.log('쿠키 토큰 존재', token)
if (token) {
const decodedToken = jwtDecode(token)
console.log('decodedToken.exp', decodedToken.exp)
const today = new Date()
const expDate = new Date(decodedToken.exp * 1000)
if (expDate && expDate >= today) {
// 토큰이 유효한 경우
this.$router.replace('/main') // 메인 페이지로 이동
} else {
// 토큰이 만료된 경우
VueCookies.remove('auth') // 토큰 삭제
}
}
},
이렇게 짰더니 token 변수에 토큰이 담기질 않는다!
시도해본 코드
1. document.cookie를 token 변수에 담아서 사용
const token = VueCookies.get('auth')
2. 함수에 document.cookie를 담아서 사용
created() {
// 이미 토큰을 가지고 있는 경우 처리를 위한 로직
function getAuthFromCookie() {
// 저장된 토큰값 가져오기
return document.cookie.replace(/(?:(?:^|.*;\s*)til_auth\s*=\s*([^;]*).*$)|^.*$/, '$1')
}
const token = getAuthFromCookie()
3. npm vue-cookies 이용
const token = VueCookies.get('auth')
강사님의 답변
쿠키는 사라질 예정이라 로컬 스토리지 이용을 추천
추가된 다음 할 일
- 로그인 실패 시 alert 처리가 입력폼 공란일 때 alert와 겹쳐 alert가 두번 뜨는 문제 수정
- 로컬 스토리지에 쿠키 담아서 연동하기
0303
- 로컬 스토리지에 쿠키 담아서 연동하기
- 로그인하면 회원가입, 로그인 대신 마이페이지, 로그아웃 보이기
store/models/auth.js
response의 data에 있는 accessToken을 받아왔다.
import api from '../apiUtil'
import jwtDecode from 'jwt-decode'
const stateInit = {
TokenUser: {
id: null,
iat: null,
exp: null
}
}
export default {
state: {
// state에 사용할 모델이나 값을 선언 한다.
TokenUser: { ...stateInit.TokenUser },
Loading: null,
Error: null
},
getters: {
// getters을 통해 state값을 호출 한다.
TokenUser: state => state.TokenUser,
TokenLoading: state => state.Loading,
Error: state => state.Error
},
mutations: {
// 동기적
setTokenUser(state, data) {
state.TokenUser = data
},
setTokenLoading(state, data) {
state.TokenLoading = data
state.Error = null
},
setError(state, data) {
state.Loading = false
state.Error = data
state.TokenUser = { ...stateInit.TokenUser }
},
clearError(state) {
state.Error = null
},
setLogout(state) {
state.Loading = false
state.Error = null
state.TokenUser = { ...stateInit.TokenUser }
}
},
actions: {
// 비동기 (mutations 호출)
// 로그인 처리
actauthLogin(context, payload) {
console.log('actTokenUser', payload) // 정보가 payload를 통해 넘어옴
// 상태(결과)값 초기화
context.commit('clearError')
context.commit('setLoading', true)
/* RestAPI 호출 */
api
.post('/serverApi/login', payload)
.then(response => {
console.log('actTokenUserAPI', response)
const token = response && response.data && response.data.accessToken
const decodedToken = jwtDecode(token)
console.log('token', decodedToken)
// 정상인 경우 처리
context.commit('setLoading', false)
context.commit('setTokenUser', decodedToken)
})
// 에러인 경우 처리
.catch(error => {
context.commit('setLoading', false)
context.commit('setError', error)
})
},
// ...
authTokenUser(context, payload) {
// 토큰 사용자 설정
const decodedToken = jwtDecode(payload)
context.commit('setTokenUser', decodedToken)
}
}
}
store/apiUtil.js
백엔드에서 보내주는 response 안의 data의 accessToken에 있는 토큰 값을 받아서 로컬 스토리지에 담았다.
accessToken을 accesstoken으로 써서 오타 나지 않도록 유의했다.
import axios from 'axios'
const api = axios.create()
// request(요청)시 아래의 로직이 인터셉트 된다.
api.interceptors.request.use(
async request => {
// header.token 전송
const token = window.localStorage.getItem('accessToken')
request.data.accesstoken = token
return request
},
async error => {
return Promise.reject(error)
}
)
// response(응답)시 아래의 로직이 인터셉트 된다.
api.interceptors.response.use(
async response => {
// data.token 자동 갱신
const token = response.data.accessToken // token을 data에서 받은 경우
if (token) {
window.localStorage.setItem('accessToken', token)
}
return response
},
async error => {
return Promise.reject(error)
}
)
export default api
결과
views/auth/login.vue
window.localStorage.getItem으로 로컬 스토리지에 있는 accessToken을 불러왔다.
// ...
created() {
// 이미 토큰을 가지고 있는 경우 처리를 위한 로직
const token = window.localStorage.getItem('accessToken')
if (token) {
const decodedToken = jwtDecode(token)
const today = new Date()
const expDate = new Date(decodedToken.exp * 1000)
if (expDate && expDate >= today) {
// 토큰이 유효한 경우
this.$router.push('/main') // 메인 페이지 이동
} else {
// 토큰이 만료된 경우
window.localStorage.removeItem('accessToken') // 토큰 삭제
}
}
},
// ...
router/index.js
라우터에서 해당 주소로 접속할 때 로그인이 상태를 체크하여 로그인 되어있을 때만(로컬 스토리지에 토큰이 존재할 때만) 해당 페이지에 접속 가능하도록 구현했다.
로그인, 메인 페이지처럼 로그인 안되어도 접속해야하는 페이지에는 meta를 사용해 next()로 그냥 넘기도록 했다.
import jwtDecode from 'jwt-decode'
import Vue from 'vue'
import VueRouter from 'vue-router'
import Main from '../views/main/Main.vue'
import store from '../store'
Vue.use(VueRouter)
const routes = [
{
path: '/',
component: Main
},
{
path: '/main',
component: Main,
meta: { noLogin: true }
},
{
path: '/reservation',
component: () => import('../views/reservation/Reservation.vue'),
name: 'goRes',
props: true
},
{
path: '/reservationcheck',
component: () => import('../views/reservation/Reservation_check.vue'),
name: 'goResCheck',
props: true
},
{
path: '/my-page',
component: () => import('../views/my_page/My_page.vue')
},
{
path: '/my-page',
component: () => import('../views/my_page/My_page.vue'),
name: 'goMy',
props: true
},
{
path: '/auth',
component: () => import('../views/auth'),
children: [
{
path: '/auth/login',
component: () => import('../views/auth/login'),
meta: { noLogin: true }
},
{
path: '/auth/sign',
component: () => import('../views/auth/signup'),
meta: { noLogin: true }
},
{
path: '/auth/logout',
component: () => import('../views/auth/logout'),
meta: { noLogin: true }
}
]
},
// NotFound 페이지 (항상 맨 밑에)
{
path: '*',
component: () => import('../components/NotFound.vue'),
meta: { noLogin: true }
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
router.beforeEach(async (to, from, next) => {
console.log('router.beforeEach', to, from)
const noLogin = to.meta.noLogin // 이동할 페이지에서 로그인 허용여부 확인
if (noLogin === true) {
// 로그인이 필요없는 페이지는 그냥 이동
next()
} else {
// 로그인이 필요한 페이지는 토큰 체크 후 통과 여부 결정
// 1. localStorage에서 토큰 추출
const token = window.localStorage.getItem('accessToken')
// TODO: 리다이렉트 페이지 처리(이동하려던 페이지로 이동시킬 수 있다.)
try {
const decodedToken = jwtDecode(token) // 토큰디코딩
const today = new Date() // 오늘날짜
const expDate = new Date(decodedToken.exp * 1000) // 토큰에서 만료일추출
if (expDate && expDate >= today) {
// 토큰이 유효한 경우
// 1. tokenUser정보가 없어진 경우 다시 갱신한다.
const tokenUser = store.getters['TokenUser']
if (!tokenUser || !tokenUser.id > 0) {
store.dispatch('authTokenUser', token)
}
// 처리를 다 했으면 가던길 가자
next()
} else {
// 토큰이 만료된 경우
next('/auth/login') // 로그인 페이지로 이동(여기에서 토큰을 삭제해준다.)
}
} catch (err) {
// 토큰 추출이 실패한 경우에 대한 처리
next('/auth/login') // 로그인 페이지로 이동
}
}
})
export default router
App.vue
로그인 했을 때 회원가입, 로그인 대신 마이페이지, 로그아웃이 보이도록 v-if에 토큰의 id값을 체크하는 함수를 넣어 구현했다.
제대로 로그인이 되어있지 않을 시엔 id 값이 null값으로 반환되기 때문에 id !== null 조건을 걸었다.
수업 땐 .Length > 0 로 구현했었는데 이번엔 이게 안먹혔다(아예 null로 반환되어서 그런지...)
// ...
<router-link to="/main"><h1 id="logo">logo</h1></router-link>
<div v-if="!isLoggedin">
<router-link to="/auth/sign">회원가입</router-link> |
<router-link to="/auth/login">로그인</router-link>
</div>
<div v-if="isLoggedin">
<router-link to="/my-page">{{ tokenUserId }}님 마이 페이지</router-link> |
<router-link to="/auth/logout">로그아웃</router-link>
</div>
// ...
<script>
export default {
computed: {
isLoggedin() {
let login = false
if (this.$store.getters.TokenUser && this.$store.getters.TokenUser.id !== null) {
login = true
}
return login
},
tokenUserId() {
return this.$store.getters.TokenUser && this.$store.getters.TokenUser.id
}
},
methods: {
onClick(path) {
this.$router.push(path)
}
}
}
</script>
결과
로그인 완료 시
로그아웃 시
로그인 필요한 페이지 접근 시(바로 예약하기 등)
0304
- 로그인/회원가입을 입력폼의 조건에 맞게 입력하지 않았을 때(ex 공란) 폼을 넘기지 않는 기능 구현
- 로그인 시 엔터 입력하여 폼 제출 구현
views/auth/login.vue
입력 폼의 조건에 맞게 입력하지 않았을 때 폼을 넘기지 않는 기능은 if 문으로 구현했다.
nullCheckInput 함수를 console.log로 찍어보니 기존 로직에서는 값이 undefined로 넘어가서 따로 true 조건을 줬다.
// ...
methods: {
// 공란 비허용
nullCheckInput() {
if (this.userLogin.loginId == null || this.userLogin.loginId == '') {
alert('아이디를 입력해 주세요')
this.$refs.loginId.focus()
return false
} else if (this.userLogin.loginPw == null || this.userLogin.loginPw == '') {
alert('비밀번호를 입력해 주세요')
this.$refs.loginPw.focus()
return false
} else {
return true
}
},
onSubmit() {
if (this.nullCheckInput() === false) {
// 아무것도 입력하지 않은 상태 체크
return false
// console.log(this.nullCheckInput())
} else {
this.$store.dispatch('actauthLogin', { ...this.userLogin })
}
}
}
// ...
결과
공란 입력시 alert와 함께 콘솔에 아무것도 찍히지 않는 걸 확인할 수 있다(api에 넘어갈 시에 store에 콘솔 로그를 걸어뒀다)
views/auth/login.vue
로그인 시 엔터 입력하여 폼 제출(넘어가기) 기능은 keyup을 폼마다 넣어서 구현했다.
어차피 입력값이 둘 중 하나라도 공란일 경우에는 값을 넘기지 않기 때문에 이렇게 짰다.
// ...
<b-form-group>
<label id="idlabel" for="loginId">아이디</label>
<b-form-input
id="loginId"
ref="loginId"
v-model="userLogin.loginId"
required
size="sm"
trim
@keyup.enter="onSubmit"
></b-form-input>
</b-form-group>
<b-form-group>
<label id="pwlabel" for="loginPw">비밀번호</label>
<b-form-input
id="loginPw"
ref="loginPw"
v-model="userLogin.loginPw"
type="password"
required
size="sm"
trim
@keyup.enter="onSubmit"
></b-form-input>
</b-form-group>
// ...
'공부 > Project' 카테고리의 다른 글
[Team 제주 넘는 차] 로그아웃 (0) | 2022.03.03 |
---|---|
[Team 제주 넘는 차] 체크박스 '전체' 항목 로직 (0) | 2022.03.03 |
[Team 제주 넘는 차] 제주도 렌터카 사이트 프로젝트 계획 (0) | 2022.02.22 |
HTML (0) | 2022.01.23 |
환경 구축 (0) | 2022.01.21 |