프로젝트의 뼈대를 만드는 작업 중이므로 HTTP에 맞는 Stateless 구현하기 위해 JWT 방식과 DB Resource 측면 및 서버 확장 가능성 등을 고려해 JWT, OAuth 로그인 방식을 구현하기로 결정 추후 필요시 Session을 추가로 구현하는 방식을 고려하며 로그인 기능을 구현할 예정이다.
화면 디자인의 경우에는 모바일과 웹에서 사용자 경험을 높일 수 있도록 반응형 웹앱 형태로 변경할 예정이다.
(HelloWorlde.vue 컴포넌트 이름도 변경할 예정, Vue 파일 관리 방식도 확인 필요)
https://www.uxpin.com/studio/blog/a-hands-on-guide-to-mobile-first-design/
우선 로그인 및 회원 가입 기능을 구현하고 메인 페이지에 반영하도록 적용한 후에 디자인은 추후 적용할 예정이다.
git branch login_signup_screen feature
가장 기본적으로 로그인 화면을 생성하고 해당 화면에서 ID, PW를 입력받도록 화면을 구성한다.
로그인 할 수 있도록 오른쪽 상단에 위치한 Toggle order 옆에 로그인 버튼을 생성하고 로그인 화면으로 갈 수 있도록 만든다.
로그인 아이콘은 뷰티파이 아이콘을 사용한다.
Where can I find a list of icons to be used in vuetify? [closed]
https://stackoverflow.com/questions/52400086/where-can-i-find-a-list-of-icons-to-be-used-in-vuetify
Material Design Icons
mdi-login / mdi-login-variant 로고를 임시로 사용한다.
뷰티파이 아이콘 적용 예제
Activator 적용 코드 (v-app-bar) 아래에 v-btn icon을 추가해준다.
<v-btn icon>
<v-icon>mdi-login</v-icon>
</v-btn>
아이콘이 적용되고 클릭도 되는 것을 확인할 수 있다. (코드는 전체 코드 참고)
<template>
<div>
Login Page
</div>
</template>
<script>
</script>
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import LoginPage from '../components/LoginPage.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
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/AboutView.vue')
},
{
path: '/login',
name: 'login',
component: LoginPage
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
<v-btn icon @click="$router.push('/login')">
<v-icon>mdi-login</v-icon>
</v-btn>
클릭 시 로그인 페이지(LoginPage.vue)로 이동하는 것을 확인했다.
뷰티파이 text-fileds 이용한다.
text-fileds 참고
elevation 참고
LoginPage.vue 코드
<template>
<v-sheet class="pa-12" rounded>
<v-card class="mx-auto px-6 py-8" max-width="60%" :elevation="12">
<v-form
v-model="form"
@submit.prevent="onSubmit"
>
<v-text-field
v-model="email"
:readonly="loading"
:rules="[required]"
class="mb-2"
clearable
label="Email"
></v-text-field>
<v-text-field
v-model="password"
:readonly="loading"
:rules="[required]"
clearable
label="Password"
placeholder="Enter your password"
></v-text-field>
<br>
<v-btn
:disabled="!form"
:loading="loading"
block
color="success"
size="large"
type="submit"
variant="elevated"
>
Sign In
</v-btn>
<br>
<v-btn
:disabled="!form"
:loading="loading"
block
color="primary"
size="large"
type="submit"
variant="elevated"
>
Sign Up
</v-btn>
</v-form>
</v-card>
</v-sheet>
</template>
<script>
</script>
SignUpPage.vue 코드
<template>
<v-sheet class="pa-12" rounded>
<v-card class="mx-auto px-6 py-8" max-width="60%" :elevation="12">
<v-form
v-model="form"
@submit.prevent="onSubmit"
>
<v-text-field
v-model="email"
:readonly="loading"
:rules="[required]"
class="mb-2"
clearable
label="Email"
></v-text-field>
<v-text-field
v-model="password"
:readonly="loading"
:rules="[required]"
clearable
label="Password"
placeholder="Enter your password"
></v-text-field>
<br>
<v-btn
:disabled="!form"
:loading="loading"
block
color="primary"
size="large"
type="submit"
variant="elevated"
>
Sign Up
</v-btn>
</v-form>
</v-card>
</v-sheet>
</template>
<script>
</script>
LoginPage.vue에서 Sign Up 버튼을 눌렀을 경우 해당 페이지로 이동할 수 있도록 index.js에서 route 설정 및 버튼 이벤트 추가
index.js 코드
...
import LoginPage from '../components/LoginPage.vue'
import SignUpPage from '../components/SignUpPage.vue'
const routes = [
...
{
path: '/login',
name: 'login',
component: LoginPage
},
{
path: '/signUp',
name: 'signUp',
component: SignUpPage
}
]
LoginPage.vue 코드
...
<v-btn
:disabled="!form"
:loading="loading"
block
color="success"
size="large"
type="submit"
variant="elevated"
>
Sign In
</v-btn>
<br>
<v-btn
:disabled="!form"
:loading="loading"
block
color="primary"
size="large"
type="submit"
variant="elevated"
@click="$router.push('/signUp')"
>
Sign Up
</v-btn>
...
로그인 페이지, 회원 가입 페이지 화면 구성 완료.
단순히 화면만 구성 완료된 것으로 Script를 통해 백엔드 통신 및 Password가 노출되지 않도록 type 지정 추가 필요.
LoginPage와 SignUpPage의 버튼에서 form을 검사하는 구문이 있다.
LoginPage에서 Sign Up의 경우에는 양식 검사 없이 바로 접근할 수 있어야 하므로 코드를 삭제한다.
:disabled="!form"
기능 구현이 완료되면 UX를 고려한 디자인도 적용할 예정...!
https://dribbble.com/shots/3902549-LOGIN-Animation-UX-Motion-Design
https://taegon.kim/archives/9658
https://github.com/cgoldsby/LoginCritter
https://www.tunnelbear.com/account/login
git add / commit / push
git add .
git commit -m "HelloWorlde.vue : 로그인 아이콘 생성,
LoginPage.vue : 로그인 페이지 화면 생성,
SignUpPage.vue : 회원가입 페이지 화면 생성,
index.js : login, signUp 경로 생성"
git push origin login_signup_screen
브랜치 정상반영 확인, Compare & pull request
pull request & Merge 이후 delete branch
<template>
<v-app id="inspire">
<v-navigation-drawer v-model="drawer">
<v-sheet
color="grey-lighten-4"
class="pa-4"
>
<v-avatar
class="mb-4"
color="grey-darken-1"
size="64"
></v-avatar>
<div>john@google.com</div>
</v-sheet>
<v-divider></v-divider>
<v-list>
<v-list-item
v-for="[icon, text] in links"
:key="icon"
link
>
<template v-slot:prepend>
<v-icon>{{ icon }}</v-icon>
</template>
<v-list-item-title>{{ text }}</v-list-item-title>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-app-bar
:order="order"
flat
title="Application bar"
>
<template v-slot:append>
<v-switch
v-model="order"
hide-details
inset
label="Toggle order"
true-value="-1"
false-value="0"
></v-switch>
</template>
<!-- Activator Slot Start -->
<v-menu>
<template v-slot:activator="{ props }">
<v-btn
color="primary"
v-bind="props"
>
Activator slot
</v-btn>
</template>
<v-list>
<v-list-item
v-for="(item, index) in items"
:key="index"
:value="index"
>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<!-- Activator Slot End -->
<!-- Activator2 Slot Start -->
<v-menu>
<template v-slot:activator="{ props }">
<v-btn
color="primary"
v-bind="props"
>
Activator2 slot
</v-btn>
</template>
<v-list>
<v-list-item
v-for="(item, index) in items2"
:key="index"
:value="index"
>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<!-- Activator2 Slot End -->
<v-btn icon>
<v-icon>mdi-login</v-icon>
</v-btn>
</v-app-bar>
<v-main class="bg-grey-lighten-2">
<v-container>
<v-row>
<template v-for="n in 4" :key="n">
<v-col
class="mt-2"
cols="12"
>
<strong>Category {{ n }}</strong>
</v-col>
<v-col
v-for="j in 6"
:key="`${n}${j}`"
cols="6"
md="2"
>
<v-sheet height="150"></v-sheet>
</v-col>
</template>
</v-row>
</v-container>
</v-main>
</v-app>
</template>
<script>
export default {
name: 'HelloWorld',
data: () => ({
order: 0,
drawer: null,
items: [
{ title: 'Item1 Click Me 1' },
{ title: 'Item1 Click Me 2' },
{ title: 'Item1 Click Me 3' },
{ title: 'Item1 Click Me 4' },
],
items2: [
{ title: 'Item2 Click Me 1' },
{ title: 'Item2 Click Me 2' },
{ title: 'Item2 Click Me 3' },
{ title: 'Item2 Click Me 4' },
],
links: [
['mdi-inbox-arrow-down', 'Inbox'],
['mdi-send', 'Send'],
['mdi-delete', 'Trash'],
['mdi-alert-octagon', 'Spam'],
],
}),
}
</script>
<template>
<v-app id="inspire">
<v-navigation-drawer v-model="drawer">
<v-sheet
color="grey-lighten-4"
class="pa-4"
>
<v-avatar
class="mb-4"
color="grey-darken-1"
size="64"
></v-avatar>
<div>john@google.com</div>
</v-sheet>
<v-divider></v-divider>
<v-list>
<v-list-item
v-for="[icon, text] in links"
:key="icon"
link
>
<template v-slot:prepend>
<v-icon>{{ icon }}</v-icon>
</template>
<v-list-item-title>{{ text }}</v-list-item-title>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-app-bar
:order="order"
flat
title="Application bar"
>
<template v-slot:append>
<v-switch
v-model="order"
hide-details
inset
label="Toggle order"
true-value="-1"
false-value="0"
></v-switch>
</template>
<!-- Activator Slot Start -->
<v-menu>
<template v-slot:activator="{ props }">
<v-btn
color="primary"
v-bind="props"
>
Activator slot
</v-btn>
</template>
<v-list>
<v-list-item
v-for="(item, index) in items"
:key="index"
:value="index"
>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<!-- Activator Slot End -->
<!-- Activator2 Slot Start -->
<v-menu>
<template v-slot:activator="{ props }">
<v-btn
color="primary"
v-bind="props"
>
Activator2 slot
</v-btn>
</template>
<v-list>
<v-list-item
v-for="(item, index) in items2"
:key="index"
:value="index"
>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<!-- Activator2 Slot End -->
<v-btn icon @click="$router.push('/login')">
<v-icon>mdi-login</v-icon>
</v-btn>
</v-app-bar>
<v-main class="bg-grey-lighten-2">
<v-container>
<v-row>
<template v-for="n in 4" :key="n">
<v-col
class="mt-2"
cols="12"
>
<strong>Category {{ n }}</strong>
</v-col>
<v-col
v-for="j in 6"
:key="`${n}${j}`"
cols="6"
md="2"
>
<v-sheet height="150"></v-sheet>
</v-col>
</template>
</v-row>
</v-container>
</v-main>
</v-app>
</template>
<script>
export default {
name: 'HelloWorld',
data: () => ({
order: 0,
drawer: null,
items: [
{ title: 'Item1 Click Me 1' },
{ title: 'Item1 Click Me 2' },
{ title: 'Item1 Click Me 3' },
{ title: 'Item1 Click Me 4' },
],
items2: [
{ title: 'Item2 Click Me 1' },
{ title: 'Item2 Click Me 2' },
{ title: 'Item2 Click Me 3' },
{ title: 'Item2 Click Me 4' },
],
links: [
['mdi-inbox-arrow-down', 'Inbox'],
['mdi-send', 'Send'],
['mdi-delete', 'Trash'],
['mdi-alert-octagon', 'Spam'],
],
}),
}
</script>
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import LoginPage from '../components/LoginPage.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
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/AboutView.vue')
},
{
path: '/login',
name: 'login',
component: LoginPage
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
LoginPage.vue
<template>
<v-sheet class="pa-12" rounded>
<v-card class="mx-auto px-6 py-8" max-width="60%" :elevation="12">
<v-form
v-model="form"
@submit.prevent="onSubmit"
>
<v-text-field
v-model="email"
:readonly="loading"
:rules="[required]"
class="mb-2"
clearable
label="Email"
></v-text-field>
<v-text-field
v-model="password"
:readonly="loading"
:rules="[required]"
clearable
label="Password"
type="password"
placeholder="Enter your password"
></v-text-field>
<br>
<v-btn
:disabled="!form"
:loading="loading"
block
color="success"
size="large"
type="submit"
variant="elevated"
>
Sign In
</v-btn>
<br>
<v-btn
:disabled="!form"
:loading="loading"
block
color="primary"
size="large"
type="submit"
variant="elevated"
@click="$router.push('/signUp')"
>
Sign Up
</v-btn>
</v-form>
</v-card>
</v-sheet>
</template>
<script>
</script>
SignUpPage.vue
<template>
<v-sheet class="pa-12" rounded>
<v-card class="mx-auto px-6 py-8" max-width="60%" :elevation="12">
<v-form
v-model="form"
@submit.prevent="onSubmit"
>
<v-text-field
v-model="email"
:readonly="loading"
:rules="[required]"
class="mb-2"
clearable
label="Email"
></v-text-field>
<v-text-field
v-model="password"
:readonly="loading"
:rules="[required]"
clearable
label="Password"
placeholder="Enter your password"
></v-text-field>
<br>
<v-btn
:disabled="!form"
:loading="loading"
block
color="primary"
size="large"
type="submit"
variant="elevated"
>
Sign Up
</v-btn>
</v-form>
</v-card>
</v-sheet>
</template>
<script>
</script>
index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import LoginPage from '../components/LoginPage.vue'
import SignUpPage from '../components/SignUpPage.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
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/AboutView.vue')
},
{
path: '/login',
name: 'login',
component: LoginPage
},
{
path: '/signUp',
name: 'signUp',
component: SignUpPage
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
HelloWorld.vue
<template>
<v-app id="inspire">
<v-navigation-drawer v-model="drawer">
<v-sheet
color="grey-lighten-4"
class="pa-4"
>
<v-avatar
class="mb-4"
color="grey-darken-1"
size="64"
></v-avatar>
<div>john@google.com</div>
</v-sheet>
<v-divider></v-divider>
<v-list>
<v-list-item
v-for="[icon, text] in links"
:key="icon"
link
>
<template v-slot:prepend>
<v-icon>{{ icon }}</v-icon>
</template>
<v-list-item-title>{{ text }}</v-list-item-title>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-app-bar
:order="order"
flat
title="Application bar"
>
<template v-slot:append>
<v-switch
v-model="order"
hide-details
inset
label="Toggle order"
true-value="-1"
false-value="0"
></v-switch>
</template>
<!-- Activator Slot Start -->
<v-menu>
<template v-slot:activator="{ props }">
<v-btn
color="primary"
v-bind="props"
>
Activator slot
</v-btn>
</template>
<v-list>
<v-list-item
v-for="(item, index) in items"
:key="index"
:value="index"
>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<!-- Activator Slot End -->
<!-- Activator2 Slot Start -->
<v-menu>
<template v-slot:activator="{ props }">
<v-btn
color="primary"
v-bind="props"
>
Activator2 slot
</v-btn>
</template>
<v-list>
<v-list-item
v-for="(item, index) in items2"
:key="index"
:value="index"
>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<!-- Activator2 Slot End -->
<v-btn icon @click="$router.push('/login')">
<v-icon>mdi-login</v-icon>
</v-btn>
</v-app-bar>
<v-main class="bg-grey-lighten-2">
<v-container>
<v-row>
<template v-for="n in 4" :key="n">
<v-col
class="mt-2"
cols="12"
>
<strong>Category {{ n }}</strong>
</v-col>
<v-col
v-for="j in 6"
:key="`${n}${j}`"
cols="6"
md="2"
>
<v-sheet height="150"></v-sheet>
</v-col>
</template>
</v-row>
</v-container>
</v-main>
</v-app>
</template>
<script>
export default {
name: 'HelloWorld',
data: () => ({
order: 0,
drawer: null,
items: [
{ title: 'Item1 Click Me 1' },
{ title: 'Item1 Click Me 2' },
{ title: 'Item1 Click Me 3' },
{ title: 'Item1 Click Me 4' },
],
items2: [
{ title: 'Item2 Click Me 1' },
{ title: 'Item2 Click Me 2' },
{ title: 'Item2 Click Me 3' },
{ title: 'Item2 Click Me 4' },
],
links: [
['mdi-inbox-arrow-down', 'Inbox'],
['mdi-send', 'Send'],
['mdi-delete', 'Trash'],
['mdi-alert-octagon', 'Spam'],
],
}),
}
</script>