Compare commits

..

No commits in common. "631629d9ddfadae95ad6c7af2e0a054e29173834" and "5ff12cab9d8df1fe90814b2ec7e2929a81af6dad" have entirely different histories.

15 changed files with 287 additions and 597 deletions

5
.env
View File

@ -8,4 +8,9 @@ VUE_APP_AUTH_HOST="http://localhost:2000"
VUE_APP_BACKEND_HOST="http://localhost:3000" VUE_APP_BACKEND_HOST="http://localhost:3000"
#LocalStorage는 Edge 브라우저의 개발툴 > 응용프로그램 > 로컬 저장소에서 확인가능 #LocalStorage는 Edge 브라우저의 개발툴 > 응용프로그램 > 로컬 저장소에서 확인가능
VUE_APP_SESSIONSTORAGE_JWT_NAME="access_token"
VUE_APP_SESSIONSTORAGE_REDIRECT_NAME = "RedirectPATH"
VUE_APP_TABLE_DEFAULT_PERPAGE = 10 VUE_APP_TABLE_DEFAULT_PERPAGE = 10
VUE_APP_TABLE_DEFAULT_TABLE_PAGE = 1

View File

@ -42,16 +42,18 @@
</b-input-group> </b-input-group>
</validation-provider> </validation-provider>
<b-button type="submit" variant="primary">Login</b-button> <b-button type="submit" variant="primary">Login</b-button>
<b-button type="reset" variant="danger" @click="onReset()"
>Reset</b-button
>
</b-form> </b-form>
</validation-observer> </validation-observer>
</div> </div>
</template> </template>
<script> <script>
// : https://kdydesign.github.io/2019/04/06/vuejs-vuex-helper/ import axios from 'axios'
export default { export default {
data() { data() {
//console.log(this.$route)
return { return {
form: { form: {
email: null, email: null,
@ -79,21 +81,66 @@ export default {
getValidationState({ dirty, validated, valid = null }) { getValidationState({ dirty, validated, valid = null }) {
return dirty || validated ? valid : null return dirty || validated ? valid : null
}, },
onSubmit() { async onSubmit() {
this.$store const result = await this.callAPI('/auth/login', {
.dispatch('AuthStore/login', { email: this.form.email,
email: this.form.email, password: this.form.password
password: this.form.password })
}) console.log(result)
if (!result) {
console.log(result)
sessionStorage.removeItem(process.env.VUE_APP_SESSIONSTORAGE_JWT_NAME)
alert('로그인 실패...')
//this.$router.back()
} else {
// localStorage , key-value Storage
sessionStorage.setItem(
process.env.VUE_APP_SESSIONSTORAGE_JWT_NAME,
result
)
alert('로그인 성공...')
// redirect path
this.$router.replace(
sessionStorage.getItem(
process.env.VUE_APP_SESSIONSTORAGE_REDIRECT_NAME
) || '/'
)
// CLear
this.onReset()
}
},
onReset() {
// Reset our form values
this.form.email = ''
this.form.pasword = ''
// redirect path Clear
sessionStorage.removeItem(
process.env.VUE_APP_SESSIONSTORAGE_REDIRECT_NAME
)
// Trick to reset/clear native browser form validation state
// this.$nextTick(() => {
// this.$refs.observer.reset()
// })
},
async callAPI(url, params) {
console.log('CallAPI..', [url, params])
axios.defaults.baseURL = process.env.VUE_APP_AUTH_HOST
// Header
const headers = {
Authorization:
'Bearer ' +
sessionStorage.getItem(process.env.VUE_APP_SESSIONSTORAGE_JWT_NAME)
}
return await axios
.post(url, params, headers)
.then((response) => { .then((response) => {
console.log(response) // console.log(response)
const return_url = this.$route.params.return_url || 'home' return response.status === 201 ? response.data : null
alert('로그인 성공... -> ' + return_url) })
this.$router.push({ name: return_url }).catch((e) => { .catch((err) => {
console.log(e) console.log(err)
}) return null
}) })
.catch(({ message }) => alert('로그인 실패...:' + message))
} }
} }
} }

View File

@ -1,104 +0,0 @@
<template>
<!-- 참조: https://codesandbox.io/s/3v0m1?file=/src/components/board/BoardList.vue -->
<b-table
ref="BTable"
:page="page"
:items="items"
:fields="fields"
:per-page="perPage"
:sort-by.sync="sortBy"
:sort-desc.sync="sortDesc"
@sort-changed="sortClick"
selectable
select-mode="multi"
@row-selected="selectedRowToggle"
:sticky-header="commonTableAttributes.stickyHeader"
:no-border-collapse="commonTableAttributes.noBorderCollapse"
:striped="commonTableAttributes.striped"
:hover="commonTableAttributes.hover"
:no-local-sorting="commonTableAttributes.noLocalSorting"
:bordered="commonTableAttributes.bordered"
:dark="commonTableAttributes.dark"
:head-variant="commonTableAttributes.headVariant"
>
<!-- Field별 Filter용 -->
<template #head(is_done)="fields">
<b-form-select
:value="filterIsDone"
:options="fields.field.Options"
@change="isDoneClick"
></b-form-select>
</template>
<!-- 비/선택 Row 토글용 -->
<template #cell(rowSelect)="{ rowSelected }">
<template v-if="rowSelected">✓</template>
</template>
<!-- Row의 Index 이용한 번호 출력용 -->
<template #cell(id)="row">
<b-link ref="edit/{{row.item.id}}">
{{ total - (page * perPage + row.index) + parseInt(perPage) }}
</b-link>
</template>
<!-- 제목부분 출력용 -->
<template #cell(title)="row">
{{ row.item.title }}
</template>
<!-- 상세 내용 출력용 -->
<template #cell(content)="row">
<b-button size="sm" @click="row.toggleDetails" class="mr-2">
[{{ row.item.id }}] {{ row.detailsShowing ? '숨기기' : '보기' }}
</b-button>
</template>
<template #row-details="row">
<b-card>
<b-row>
<b-col v-html="row.item.content"></b-col>
</b-row>
<b-button size="sm" @click="row.toggleDetails">숨기기</b-button>
</b-card>
</template>
<!-- 사용여부 출력용 -->
<template #cell(is_done)="row">
{{ row.value }}
</template>
</b-table>
</template>
<script>
export default {
name: 'TableComponent',
props: [
'fields',
'items',
'total',
'page',
'perPage',
'selectedRows',
'filterIsDone'
],
data: function () {
return {
perPageOptions: [
{ text: '5줄', value: 5 },
{ text: '10줄', value: 10 },
{ text: '30줄', value: 30 },
{ text: '60줄', value: 60 },
{ text: '100줄', value: 100 }
],
// 속성옵션
commonTableAttributes: {
stickyHeader: '50%',
//noBorderCollapse: false,
striped: true,
hover: true,
noLocalSorting: true,
bordered: true,
dark: false,
headVariant: 'light'
},
sortBy: 'id',
sortDesc: true
}
}
}
</script>
<style scoped></style>

View File

@ -1,87 +0,0 @@
<template>
<b-container fluid>
<b-row>
<b-col class="border">
<b-input-group>
<label class="input-group-text">
<b-form-checkbox @change="selectedRowAllToggle"
>All</b-form-checkbox
>
</label>
</b-input-group>
</b-col>
<b-col cols="4" class="border">
<b-input-group>
<label class="input-group-text">
<b-form-checkbox-group
class="d-flex flex-nowrap"
v-model="searchFields"
:options="searchFieldOptions"
></b-form-checkbox-group>
</label>
<b-form-input
v-model="search"
type="text"
placeholder="검색어"
@keydown.enter="searchClick"
></b-form-input>
<b-button @click="searchClick">검색</b-button>
</b-input-group>
</b-col>
<b-col cols="4" class="border">
<b-input-group>
<b-form-select
class="d-flex flex-nowrap"
:value="filterDateField"
:options="filterDateFieldOptions"
></b-form-select>
<b-form-input :value="filterDateStart" type="date"></b-form-input>
~
<b-form-input :value="filterDateEnd" type="date"></b-form-input>
<b-button @click="dateClick">선택</b-button>
</b-input-group>
</b-col>
<b-col class="border">
<b-input-group class="justify-content-end">
<label class="input-group-text">줄수</label>
<b-form-select
:value="perPage"
:options="perPageOptions"
@change="perPageClick"
></b-form-select>
</b-input-group>
</b-col>
</b-row>
</b-container>
</template>
<script>
export default {
name: 'TableHeaderComponent',
props: [
'searchFields',
'searchFieldOptions',
'filterDateField',
'filterDateFieldOptions',
'filterDateStart',
'filterDateEnd'
],
data: function () {
return {
perPageOptions: [
{ text: '5줄', value: 5 },
{ text: '10줄', value: 10 },
{ text: '30줄', value: 30 },
{ text: '60줄', value: 60 },
{ text: '100줄', value: 100 }
],
search: null,
filter: null,
filterField: null,
filterDateStart: null,
filterDateEnd: null,
selectedRowAll: false
}
}
}
</script>
<style scoped></style>

View File

@ -1,20 +0,0 @@
<template>
<div>
<b-pagination
:total-rows="total"
:page="page"
:per-page="perPage"
@page-click="pageClick"
last-number
class="justify-content-center"
>
</b-pagination>
</div>
</template>
<script>
export default {
name: 'TablePaginationComponent',
props: ['total', 'page', 'perPage', 'pageClick']
}
</script>
<style scoped></style>

View File

@ -31,51 +31,20 @@
<b-dropdown-item href="#">FA</b-dropdown-item> <b-dropdown-item href="#">FA</b-dropdown-item>
</b-nav-item-dropdown> --> </b-nav-item-dropdown> -->
<template v-if="isAuthenticated"> <b-nav-item-dropdown right>
<b-nav-item-dropdown right> <!-- Using 'button-content' slot -->
<!-- Using 'button-content' slot --> <template #button-content>
<template #button-content> <em>User</em>
<em>User</em> </template>
</template> <b-dropdown-item href="#">Profile</b-dropdown-item>
<b-dropdown-item href="/profile">Profile</b-dropdown-item> <b-dropdown-item href="#">Sign Out</b-dropdown-item>
<b-dropdown-item @click="logout()">Sign Out</b-dropdown-item> </b-nav-item-dropdown>
</b-nav-item-dropdown>
</template>
<template v-else
><b-dropdown-item href="/login"
>Login {{ isAuthenticated }}</b-dropdown-item
>
</template>
</b-navbar-nav> </b-navbar-nav>
</b-collapse> </b-collapse>
</b-navbar> </b-navbar>
</template> </template>
<script> <script>
import { createNamespacedHelpers } from 'vuex'
const authStore = createNamespacedHelpers('AuthStore')
export default { export default {
name: 'HeaderLayout', name: 'HeaderLayout'
computed: {
...authStore.mapGetters(['isAuthenticated'])
},
methods: {
redirect() {
if (!this.isAuthenticated && this.$router.name != 'home') {
this.$router
.push({
name: 'home'
})
.catch((e) => {
console.log(e + ':' + this.$router)
})
}
},
logout() {
this.$store
.dispatch('AuthStore/logout', {})
.then(() => this.redirect())
.catch(({ message }) => alert(message))
}
}
} }
</script> </script>

View File

@ -1,58 +1,74 @@
<template> <template>
<validation-observer ref="observer" v-slot="{ handleSubmit }"> <div>
<b-form @submit.stop.prevent="handleSubmit(onSubmit)"> <b-modal
<validation-provider id="inputForm"
:name="field.title.label" centered
:rules="field.title.rules" title="입력폼"
v-slot="validationContext" size="lg"
> :header-bg-variant="headerBgVariant"
<b-input-group> :header-text-variant="headerTextVariant"
<label class="input-group-text">{{ field.title.label }}</label> :body-bg-variant="bodyBgVariant"
<b-form-input :body-text-variant="bodyTextVariant"
:name="field.title.key" :footer-bg-variant="footerBgVariant"
:type="field.title.type" :footer-text-variant="footerTextVariant"
v-model="form.title" >
:state="getValidationState(validationContext)" <validation-observer ref="observer" v-slot="{ handleSubmit }">
:style="field.title.style" <b-form @submit.stop.prevent="handleSubmit(onSubmit)">
></b-form-input> <validation-provider
<b-form-invalid-feedback>{{ :name="field.title.label"
validationContext.errors[0] :rules="field.title.rules"
}}</b-form-invalid-feedback> v-slot="validationContext"
</b-input-group> >
</validation-provider> <b-input-group>
<validation-provider <label class="input-group-text">{{ field.title.label }}</label>
:name="field.content.label" <b-form-input
:rules="field.content.rules" :name="field.title.key"
v-slot="validationContext" :type="field.title.type"
> v-model="form.title"
<label class="input-group-text">{{ field.content.label }}</label> :state="getValidationState(validationContext)"
<b-input-group> :style="field.title.style"
<b-form-textarea ></b-form-input>
:config="field.content.config" <b-form-invalid-feedback>{{
:name="field.content.key" validationContext.errors[0]
v-model="form.content" }}</b-form-invalid-feedback>
:state="getValidationState(validationContext)" </b-input-group>
:style="field.content.style" </validation-provider>
rows="8" <validation-provider
></b-form-textarea> :name="field.content.label"
<b-form-invalid-feedback>{{ :rules="field.content.rules"
validationContext.errors[0] v-slot="validationContext"
}}</b-form-invalid-feedback> >
</b-input-group> <label class="input-group-text">{{ field.content.label }}</label>
</validation-provider> <b-input-group>
<div class="mt-300 border"> <b-form-textarea
<b-button type="submit" variant="primary">입력</b-button> :config="field.content.config"
<b-button type="reset" variant="danger" @click="onReset()" :name="field.content.key"
>Reset</b-button v-model="form.content"
> :state="getValidationState(validationContext)"
</div> :style="field.content.style"
</b-form> rows="8"
</validation-observer> ></b-form-textarea>
<b-form-invalid-feedback>{{
validationContext.errors[0]
}}</b-form-invalid-feedback>
</b-input-group>
</validation-provider>
<div class="mt-300 border">
<b-button type="submit" variant="primary">입력</b-button>
<b-button type="reset" variant="danger" @click="onReset()"
>Reset</b-button
>
</div>
</b-form>
</validation-observer>
</b-modal>
</div>
</template> </template>
<script> <script>
import axios from 'axios' import axios from 'axios'
//import { VueEditor } from 'vue2-editor/dist/vue2-editor.core.js' //import { VueEditor } from 'vue2-editor/dist/vue2-editor.core.js'
export default { export default {
components: {}, components: {},
data() { data() {
@ -77,7 +93,23 @@ export default {
rules: { required: true, min: 10 }, rules: { required: true, min: 10 },
style: 'height: 200px' style: 'height: 200px'
} }
} },
variants: [
'primary',
'secondary',
'success',
'warning',
'danger',
'info',
'light',
'dark'
],
headerBgVariant: 'dark',
headerTextVariant: 'light',
bodyBgVariant: 'light',
bodyTextVariant: 'dark',
footerBgVariant: 'warning',
footerTextVariant: 'dark'
} }
}, },
methods: { methods: {

View File

@ -60,9 +60,10 @@
</b-row> </b-row>
</b-container> </b-container>
<b-table <b-table
ref="BTable" id="todoTable"
ref="todoTable"
:page="page" :page="page"
:items="getRows" :items="rows"
:fields="fields" :fields="fields"
:per-page="perPage" :per-page="perPage"
:sort-by.sync="sortBy" :sort-by.sync="sortBy"
@ -74,6 +75,7 @@
:select-mode="selectedRowMode" :select-mode="selectedRowMode"
selectable selectable
@row-selected="rowSelectedToggle" @row-selected="rowSelectedToggle"
:busy="isBusy"
:sticky-header="commonTableAttributes.stickyHeader" :sticky-header="commonTableAttributes.stickyHeader"
:no-border-collapse="commonTableAttributes.noBorderCollapse" :no-border-collapse="commonTableAttributes.noBorderCollapse"
:striped="commonTableAttributes.striped" :striped="commonTableAttributes.striped"
@ -93,12 +95,13 @@
</template> </template>
<!-- /선택 Row 토글용 --> <!-- /선택 Row 토글용 -->
<template #cell(rowSelect)="{ rowSelected }"> <template #cell(rowSelect)="{ rowSelected }">
<template v-if="rowSelected"></template> <template v-if="rowSelected"> </template>
<template v-else> &nbsp; </template>
</template> </template>
<!-- Row의 Index 이용한 번호 출력용 --> <!-- Row의 Index 이용한 번호 출력용 -->
<template #cell(id)="row"> <template #cell(id)="row">
<b-link ref="edit/{{row.item.id}}"> <b-link ref="edit/{{row.item.id}}">
{{ getTotal - (page * perPage + row.index) + Number(perPage) }} {{ total - (page * perPage + row.index) + perPage }}
</b-link> </b-link>
</template> </template>
<!-- 제목부분 출력용 --> <!-- 제목부분 출력용 -->
@ -123,6 +126,13 @@
<template #cell(is_done)="row"> <template #cell(is_done)="row">
{{ row.value }} {{ row.value }}
</template> </template>
<!-- 데이터 로딩중 출력용 -->
<template #table-busy>
<div class="text-center text-danger my-2">
<b-spinner class="align-middle"></b-spinner>
<strong>Loading...</strong>
</div>
</template>
</b-table> </b-table>
<b-container fluid> <b-container fluid>
<b-row> <b-row>
@ -132,33 +142,32 @@
<b-col> <b-col>
<b-pagination <b-pagination
:page="page" :page="page"
:total-rows="getTotal" :total-rows="total"
:per-page="perPage" :per-page="perPage"
@page-click="pageClick" @page-click="pageClick"
last-number last-number
aria-controls="todoTable"
class="justify-content-center" class="justify-content-center"
> >
</b-pagination> </b-pagination>
</b-col> </b-col>
<b-col> </b-col> <b-col>
<b-input-group class="justify-content-end">
<b-button @click="busyToggle">Toggle Busy State</b-button>
</b-input-group>
</b-col>
</b-row> </b-row>
</b-container> </b-container>
</div> </div>
</template> </template>
<script> <script>
import { createNamespacedHelpers } from 'vuex'
const todoStore = createNamespacedHelpers('TodoStore')
// : https://vuejsexamples.com/vuejs-tables-and-select-all-checkbox/ // : https://vuejsexamples.com/vuejs-tables-and-select-all-checkbox/
import axios from 'axios'
export default { export default {
components: {}, components: {},
computed: {
...todoStore.mapGetters(['getTotal', 'getRows'])
},
created: function () {
this.setDatas()
},
data: function () { data: function () {
return { return {
rows: [],
fields: [ fields: [
{ {
key: 'rowSelect', key: 'rowSelect',
@ -212,7 +221,8 @@ export default {
sortByFormatted: true //fomatter Sort true sortByFormatted: true //fomatter Sort true
} }
], ],
page: 1, // total: 0,
page: process.env.VUE_APP_TABLE_DEFAULT_PAGE, //
perPage: process.env.VUE_APP_TABLE_DEFAULT_PERPAGE, // perPage: process.env.VUE_APP_TABLE_DEFAULT_PERPAGE, //
perPageOptions: [ perPageOptions: [
{ text: '5줄', value: 5 }, { text: '5줄', value: 5 },
@ -232,6 +242,7 @@ export default {
dark: false, dark: false,
headVariant: 'light' headVariant: 'light'
}, },
isBusy: false,
sortBy: 'id', sortBy: 'id',
sortDesc: true, sortDesc: true,
search: null, search: null,
@ -256,10 +267,17 @@ export default {
selectedRowModes: ['multi', 'single', 'range'] selectedRowModes: ['multi', 'single', 'range']
} }
}, },
setup: function () {},
created: function () {},
mounted: function () {
this.getDatas()
},
unmounted() {},
methods: { methods: {
setDatas(page = 1) { async getDatas(page = 1) {
this.$store this.isBusy = true
.dispatch('TodoStore/setData', { const results = await this.callAPI('/todo', {
params: {
page: page, page: page,
perPage: this.perPage, perPage: this.perPage,
sortBy: this.sortBy, sortBy: this.sortBy,
@ -270,60 +288,90 @@ export default {
filterField: this.filterField, filterField: this.filterField,
filterDateStart: this.filterDateStart, filterDateStart: this.filterDateStart,
filterDateEnd: this.filterDateEnd filterDateEnd: this.filterDateEnd
}) }
.catch((error) => { })
alert(error) //console.log(JSON.stringify(results))
}) console.log(results)
this.total = results.total
this.perPage = results.perPage
this.page = results.page
this.sortBy = results.sortBy
this.sortDesc = results.sortDesc === 'true' ? true : false
this.rows = results.rows
this.isBusy = false
}, },
searchClick() { searchClick() {
this.setDatas() this.getDatas()
}, },
pageClick(event, page) { pageClick(bvEvent, page) {
//console.log(event) console.log(bvEvent)
this.setDatas(page) this.getDatas(page)
// event.preventDefault() // bvEvent.preventDefault()
}, },
perPageClick(perPage) { perPageClick(perPage) {
this.perPage = perPage this.perPage = perPage
//console.log(this.perPage) console.log(this.perPage)
this.setDatas() this.getDatas()
}, },
sortClick(event) { sortClick(event) {
//console.log(event) console.log(event)
this.sortBy = event.sortBy this.sortBy = event.sortBy
this.sortDesc = event.sortDesc this.sortDesc = event.sortDesc
this.setDatas() this.getDatas()
}, },
dateClick() { dateClick() {
this.filterField = this.filterDateField this.filterField = this.filterDateField
this.setDatas() this.getDatas()
}, },
isDoneClick() { isDoneClick() {
this.filter = this.filterIsDone this.filter = this.filterIsDone
this.filterField = 'is_done' this.filterField = 'is_done'
this.setDatas() this.getDatas()
},
busyToggle() {
this.isBusy = !this.isBusy
}, },
rowSelectedToggle(selectedRow) { rowSelectedToggle(selectedRow) {
this.selectedRows = selectedRow this.selectedRows = selectedRow
}, },
rowSelectAllToggle() { rowSelectAllToggle() {
if (this.selectedRowAllToggle) { if (this.selectedRowAllToggle) {
this.$refs.BTable.clearSelected() this.$refs.todoTable.clearSelected()
this.selectedRowAllToggle = false this.selectedRowAllToggle = false
} else { } else {
this.$refs.BTable.selectAllRows() this.$refs.todoTable.selectAllRows()
this.selectedRowAllToggle = true this.selectedRowAllToggle = true
} }
} },
// rowSelectSet(id) { // rowSelectSet(id) {
// // Rows are indexed from 0, so the third row is index 2 // // Rows are indexed from 0, so the third row is index 2
// this.$refs.BTable.selectRow(id) // this.$refs.todoTable.selectRow(id)
// }, // },
// rowSelectUnSet(id) { // rowSelectUnSet(id) {
// // Rows are indexed from 0, so the third row is index 2 // // Rows are indexed from 0, so the third row is index 2
// this.$refs.BTable.unselectRow(id) // this.$refs.todoTable.unselectRow(id)
// } // }
} async callAPI(url, params) {
console.log('CallAPI..', [url, params])
axios.defaults.baseURL = process.env.VUE_APP_BACKEND_HOST
// Header
const headers = {
Authorization:
'Bearer ' +
sessionStorage.getItem(process.env.VUE_APP_LOCALSTORAGE_NAME)
}
return await axios
.get(url, params, headers)
.then((response) => {
return response.status === 200 ? response.data : []
})
.catch((err) => {
console.log(err)
return []
})
}
},
computed: {}
} }
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -1,7 +1,7 @@
import Vue from 'vue' import Vue from 'vue'
import VueRouter from 'vue-router' import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
import store from '../store'
Vue.use(VueRouter) Vue.use(VueRouter)
const routes = [ const routes = [
@ -33,8 +33,8 @@ const routes = [
component: () => component: () =>
import( import(
/* webpackChunkName: "api", webpackPrefetch:true */ '@/views/todoView.vue' /* webpackChunkName: "api", webpackPrefetch:true */ '@/views/todoView.vue'
), )
meta: { requiredAuth: true } //meta: { requiredAuth: true }
} }
] ]
@ -52,6 +52,21 @@ const router = new VueRouter({
// 1. 전역가드 : router.beforeEach((to, from, next) 이런식으로 전체에 대해 특정조건 (meta: { requiredAuth: true })으로 사용 // 1. 전역가드 : router.beforeEach((to, from, next) 이런식으로 전체에 대해 특정조건 (meta: { requiredAuth: true })으로 사용
// 2. 라우터 가드: 특정 Route Path아래에 직접선언-> beforeEach: function(to, from, next){} // 2. 라우터 가드: 특정 Route Path아래에 직접선언-> beforeEach: function(to, from, next){}
// 3. 컴포넌트 가드: 특정 vue 파일내에서 바로 선언해서 사용 :// view/Mypage.vue ( Mypage Components ) // 3. 컴포넌트 가드: 특정 vue 파일내에서 바로 선언해서 사용 :// view/Mypage.vue ( Mypage Components )
// export default {
// name: "Mypage",
// beforeRouteEnter(to, from, next) {
// // Login 컴포넌트가 화면에 표시되기 전에 수행될 로직
// // Login 컴포넌트는 아직 생성되지 않은 시점
// },
// beforeRouteUpdate(to, from, next) {
// // 화면에 표시된 컴포넌트가 변경될 때 수행될 로직
// // `this`로 Login 컴포넌트를 접근할 수 있음
// },
// beforeRouteLeave(to, from, next) {
// // Login 컴포넌트를 화면에 표시한 url 값이 변경되기 직전의 로직
// // `this`로 Login 컴포넌트를 접근할 수 있음
// },
// }
// 로그인 처리 과정용 // 로그인 처리 과정용
// to : 이동할 url 정보가 담긴 라우터 객체 // to : 이동할 url 정보가 담긴 라우터 객체
// from : 현재 url 정보가 담긴 라우터 객체 // from : 현재 url 정보가 담긴 라우터 객체
@ -59,21 +74,26 @@ const router = new VueRouter({
//여기에서 login은 path가 아니라 name에서 찾는다 //여기에서 login은 path가 아니라 name에서 찾는다
//router.beforeEach()를 호출하고 나면 모든 라우팅이 대기 상태가 된다 //router.beforeEach()를 호출하고 나면 모든 라우팅이 대기 상태가 된다
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
console.log('라우딩 대기')
//1. routes 설정에서 meta: { requiredAuth: true } 가 선언된 경우 //1. routes 설정에서 meta: { requiredAuth: true } 가 선언된 경우
if (to.matched.some((route) => route.meta.requiredAuth)) { if (to.matched.some((routeRecord) => routeRecord.meta.requiredAuth)) {
//2. 로그인 인증 않된 경우 //2. 로그인 인증 않된 경우
//console.log(store.getters) //sessionStorage Access-Token이 없으면 Login페이지로 전송
//console.log(store.getters['AuthStore/isAuthenticated']) if (!sessionStorage.getItem(process.env.VUE_APP_SESSIONSTORAGE_JWT_NAME)) {
if (!store.getters['AuthStore/isAuthenticated']) { console.log(from.path + ' => 3. Login 페이지 이동 => 로그인 페이지')
console.log(from.name + ' => 3. To[' + to.name + '] => 로그인') //로그인 성공 후 이동할 URL 저장
next({ name: 'login', params: { return_url: to.name } }) sessionStorage.setItem(
process.env.VUE_APP_SESSIONSTORAGE_REDIRECT_NAME,
to.path
)
next({ name: 'login' })
} else { } else {
//3. 로그인 인증완료 //3. 로그인 인증완료
console.log(from.name + ' => 2. 이미인증완료 => ' + to.name) console.log(from.path + ' => 2. 이미인증완료 => ' + to.path)
next() next()
} }
} else { } else {
console.log(from.name + ' => 1. 인증요구 없음 => ' + to.name) console.log(from.path + ' => 1. 인증요구 없음 => ' + to.path)
next() next()
} }
}) })

View File

@ -1,13 +1,12 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import AuthStore from './modules/auth'
import TodoStore from './modules/todo'
Vue.use(Vuex) Vue.use(Vuex)
export default new Vuex.Store({ export default new Vuex.Store({
modules: { state: {},
AuthStore, getters: {},
TodoStore mutations: {},
} actions: {},
modules: {}
}) })

View File

@ -1,97 +0,0 @@
import axios from 'axios'
import jwt from './jwt'
//참조: https://yamoo9.github.io/axios/guide/interceptors.html
const authAxios = axios.create({
baseURL: process.env.VUE_APP_AUTH_HOST,
headers: { 'content-type': 'application/json' }
})
authAxios.interceptors.request.use(
(config) => jwt.getAuthorizationHeader(config),
(error) => {
// Do something with request error
Promise.reject(error)
}
)
authAxios.defaults.headers.post['Content-Type'] =
'application/x-www-form-urlencoded'
// count state 속성 추가
const state = {
token: {
accessToken: jwt.getToken()
}, // 토큰정보
isAuthenticated: jwt.isAuthenticated()
}
const getters = {
getAccessToken: function (state) {
return state.token.accessToken
},
isAuthenticated: function (state) {
return state.isAuthenticated
}
}
const mutations = {
login: function (state, payload = {}) {
state.token.accessToken = payload.accessToken
state.isAuthenticated = true
jwt.saveToken(payload.accessToken)
},
logout: function (state) {
state.token.accessToken = ''
state.isAuthenticated = false
jwt.destroyToken()
}
}
const actions = {
login: function (context, payload) {
let params = {
email: payload.email,
password: payload.password
}
return new Promise((resolve, reject) => {
authAxios
.post('/auth/login', params)
.then((response) => {
const { data } = response
context.commit('login', {
accessToken: data.accessToken
})
resolve(response)
})
.catch((error) => {
reject(error)
})
})
},
logout: function (context, payload) {
return new Promise((resolve) => {
setTimeout(function () {
context.commit('logout', payload)
resolve({})
}, 1000)
})
},
register: function (context, payload) {
let params = {
email: payload.email,
password: payload.password,
name: payload.name
}
return new Promise((resolve, reject) => {
authAxios
.post('/user/register', params)
.then((response) => {
const { data } = response
context.commit('login', {
accessToken: data.accessToken
})
resolve(response)
})
.catch((error) => {
reject(error)
})
})
}
}
export default { namespaced: true, state, getters, mutations, actions }

View File

@ -1,28 +0,0 @@
const Payload_KEY_NAME = 'access_token'
const getToken = () => {
return window.localStorage.getItem(Payload_KEY_NAME)
}
const saveToken = (token) => {
window.localStorage.setItem(Payload_KEY_NAME, token)
}
const destroyToken = () => {
window.localStorage.removeItem(Payload_KEY_NAME)
}
const isAuthenticated = () => {
return !!getToken()
}
const getAuthorizationHeader = (config) => {
//로그인이 되었는지 확인후 Request시 헤더에 로그인정보 추가하기 위함
if (isAuthenticated) {
config.headers.common['Authorization'] = getToken()
}
return config
}
export default {
getToken,
saveToken,
destroyToken,
isAuthenticated,
getAuthorizationHeader
}

View File

@ -1,56 +0,0 @@
import axios from 'axios'
import jwt from './jwt'
//참조: https://yamoo9.github.io/axios/guide/interceptors.html
const todoAxios = axios.create({
baseURL: process.env.VUE_APP_BACKEND_HOST,
headers: { 'content-type': 'application/json' }
})
todoAxios.interceptors.request.use(
(config) => jwt.getAuthorizationHeader(config),
(error) => {
// Do something with request error
Promise.reject(error)
}
)
todoAxios.defaults.headers.post['Content-Type'] =
'application/x-www-form-urlencoded'
// count state 속성 추가
const state = {
total: 0,
rows: []
}
const getters = {
getTotal: function (state) {
return state.total
},
getRows: function (state) {
return state.rows
}
}
const mutations = {
setData: function (state, data) {
state.total = data.total
state.rows = data.rows
}
}
const actions = {
setData: function (context, params) {
console.log(params)
//주의 .get의 parameter는 { params: params} 처럼 보내야함
return new Promise((resolve, reject) => {
todoAxios
.get('/todo', { params: params })
.then((response) => {
const { data } = response
context.commit('setData', data)
resolve(response)
})
.catch((error) => {
reject(error)
})
})
}
}
export default { namespaced: true, state, getters, mutations, actions }

View File

@ -1,23 +1,15 @@
<template> <template>
<div class="wrapper"> <div>
<!-- 참조: https://codesandbox.io/s/3v0m1?file=/src/components/board/BoardList.vue --> <!-- 참조: https://codesandbox.io/s/3v0m1?file=/src/components/board/BoardList.vue -->
<LoginComponent /> <LoginComponent />
</div> </div>
</template> </template>
<script> <script>
import LoginComponent from '@/components/LoginComponent.vue' import LoginComponent from '@/components/common/LoginComponent.vue'
export default { export default {
components: { components: {
LoginComponent LoginComponent
} }
} }
</script> </script>
<style scoped> <style scoped></style>
.wrapper {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: white;
}
</style>

View File

@ -3,48 +3,18 @@
<!-- 참조: https://codesandbox.io/s/3v0m1?file=/src/components/board/BoardList.vue --> <!-- 참조: https://codesandbox.io/s/3v0m1?file=/src/components/board/BoardList.vue -->
<ListComponent /> <ListComponent />
<div class="justify-content-end border"> <div class="justify-content-end border">
<template v-if="isAuthenticated"> <b-button v-b-modal.inputForm variant="outline-primary">입력</b-button>
<b-button v-b-modal.inputForm variant="outline-primary">입력</b-button> <InputComponent />
<b-modal
id="inputForm"
centered
title="입력폼"
size="lg"
:header-bg-variant="headerBgVariant"
:header-text-variant="headerTextVariant"
:body-bg-variant="bodyBgVariant"
:body-text-variant="bodyTextVariant"
:footer-bg-variant="footerBgVariant"
:footer-text-variant="footerTextVariant"
>
<InputComponent />
</b-modal>
</template>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import ListComponent from '@/components/todo/ListComponent.vue' import ListComponent from '@/components/todo/ListComponent.vue'
import InputComponent from '@/components/todo/InputComponent.vue' import InputComponent from '@/components/todo/InputComponent.vue'
import { createNamespacedHelpers } from 'vuex'
const authStore = createNamespacedHelpers('AuthStore')
export default { export default {
components: { components: {
InputComponent, InputComponent,
ListComponent ListComponent
},
computed: {
...authStore.mapGetters(['isAuthenticated'])
},
data() {
return {
headerBgVariant: 'dark',
headerTextVariant: 'light',
bodyBgVariant: 'light',
bodyTextVariant: 'dark',
footerBgVariant: 'warning',
footerTextVariant: 'dark'
}
} }
} }
</script> </script>