Spring Boot
인텔리제이에서 간단하게 DataBase 사용하기
- View - Tool Windows - Database
- Data Source
로그인
JWT 적용하고, 쿠키에 토큰 저장
개발자 도구에서 응용프로그램(애플리케이션)에서 확인가능
컨트롤러
@RestController
public class AccountController {
@Autowired
private MemberRepository memberRepository;
@PostMapping("/api/account/login")
public ResponseEntity<Integer> login(@RequestBody Map<String,String> loginDto, HttpServletResponse httpServletResponse){
Member member = memberRepository.findByEmailAndPassword(loginDto.get("email"), loginDto.get("password"))
.orElseThrow(()-> new ResponseStatusException(HttpStatus.NOT_FOUND));
JwtService jwtService = new JwtService();
int id = member.getId();
String token = jwtService.getToken("id",id);
Cookie cookie = new Cookie("token", token);
cookie.setHttpOnly(true);
cookie.setPath("/");
httpServletResponse.addCookie(cookie);
return ResponseEntity.ok(id);
}
}
JwtService
public class JwtService {
private String secretKey = "javapp1234dlrjtdmstlzmfltzldlekdl&&rmflrhdlrjtdms256qlxmdltkddmlanswkdufdmfrkwlrhdlTdjdiehlsek";
public String getToken(String key, Object value){
Date expTime = new Date();
expTime.setTime(expTime.getTime() + 1000 * 60 * 5);
byte[] secretByteKey = DatatypeConverter.parseBase64Binary(secretKey);
Key signKey = new SecretKeySpec(secretByteKey, SignatureAlgorithm.HS256.getJcaName());
Map<String, Object> headerMap = new HashMap<>();
headerMap.put("type","JWT");
headerMap.put("alg", "HS256");
Map<String, Object> map = new HashMap<>();
map.put(key, value);
JwtBuilder builder = Jwts.builder().setHeader(headerMap)
.setClaims(map)
.setExpiration(expTime)
.signWith(signKey, SignatureAlgorithm.HS256);
return builder.compact();
}
}
Vue 화면 구현
라우터 (vue-router)를 사용해 각 path마다 뷰를 등록
scripts - router.js
import Home from "@/pages/Home";
import Login from "@/pages/Login";
import {createRouter, createWebHistory} from "vue-router";
const routes= [
{path: '/', component: Home},
{path: '/login', component: Login}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router;
Home.vue
<template>
<div class="home">
<div class="album py-5 bg-light">
<div class="container">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
<div class="col" v-for="(item, idx) in state.items" :key="idx">
<Card :item="item"/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Card from "@/components/Card";
import axios from "axios"
import {reactive} from "vue";
export default {
name: "Home",
components: {Card},
setup(){
const state = reactive({
items:[]
})
axios.get("/api/items").then(({data})=>{
state.items = data
})
return {state}
}
}
</script>
<style scoped>
</style>
v-for 으로 Card 컴포넌트를 반복
<Card :item="item"/>
각 Card 컴포넌트에는 item을 넘겨주었습니다.
setup(){
const state = reactive({
items:[]
})
axios.get("/api/items").then(({data})=>{
state.items = data
})
return {state}
}
setup 내에 js 코드로 로직을 작성할 수 있음
reactive는 리액트의 useState와 비슷한 거 같음
axios를 통해 서버와 비동기 통신을 하고 Card에 대한 json 값을 받아옴
return으로 넘기면 template에서 사용할 수 있음
Card.vue
파라미터로 넘긴 데이터를 props를 통해 받아옵니다.
<template xmlns:>
<div class="card shadow-sm">
<span class="img" :style="{backgroundImage: `url(${item.imgPath})`}" />
<div class="card-body">
<p class="card-text">{{item.name}}</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-primary">구입하기</button>
</div>
<small class="price text-muted">{{ lib.getNumberFormatted(item.price) }}원</small>
<small class="discount badge bg-danger ">{{ lib.getNumberFormatted(item.price - (item.price * item.discountPer / 100)) }}</small>
<small class="real text-danger ">{{ item.discountPer }}%</small>
</div>
</div>
</div>
</template>
<script>
import lib from "@/scripts/lib";
export default {
name: "Card",
props:{
item: Object
},
setup(){
return {lib}
}
}
</script>
<style scoped>
.card .img{
display: inline-block;
width: 100%;
height: 250px;
background-size: cover;
background-position: center;
}
.card .card-body .price{
text-decoration: line-through;
}
</style>
가격에 대한 숫자 포맷을 js 메소드를 통해 변경했습니다.
scripts - lib.js
export default {
getNumberFormatted(val){
return val.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
}
로그인 과정
Login.vue
<template>
<div class="form-signin">
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
<label for="inputEmail" class="sr-only">Email address</label>
<input type="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus v-model="state.form.email">
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" id="inputPassword" class="form-control" placeholder="Password" required v-model="state.form.password">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me"> Remember me
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" @click="submit()">Sign in</button>
<p class="mt-5 mb-3 text-muted">© 2017-2018</p>
</div>
</template>
<script>
import {reactive} from "vue";
import axios from "axios";
import store from "@/scripts/store";
import router from "@/scripts/router";
export default {
name: "Login",
setup(){
const state = reactive({
form:{
email:"",
password:""
}
})
const submit = () =>{
axios.post('/api/account/login',state.form).then((res)=>{
store.commit('setAccount', res.data); // id 가 넘어온다.
router.push({path:"/"})
window.alert("로그인 완료")
}).catch(()=>{
window.alert("로그인 정보 없음")
})
}
return {state, submit}
}
}
</script>
<style scoped>
.form-signin {
width: 100%;
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
.form-signin .checkbox {
font-weight: 400;
}
.form-signin .form-control {
position: relative;
box-sizing: border-box;
height: auto;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
</style>
input 태그로 입력받은 이메일과 패스워드 값은 v-model 을 사용해 담아둠
<label for="inputEmail" class="sr-only">Email address</label>
<input type="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus v-model="state.form.email">
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" id="inputPassword" class="form-control" placeholder="Password" required v-model="state.form.password">
이렇게 사용하기 위해서 setup에 form 을 구조체같이 만들어 줌
setup(){
const state = reactive({
form:{
email:"",
password:""
}
})
@click="submit()" 클릭 이벤트
<button class="btn btn-lg btn-primary btn-block" @click="submit()">Sign in</button>
setup에서 submit 메소드를 작성하고 return
const submit = () =>{
axios.post('/api/account/login',state.form).then((res)=>{
store.commit('setAccount', res.data); // id 가 넘어온다.
router.push({path:"/"})
window.alert("로그인 완료")
}).catch(()=>{
window.alert("로그인 정보 없음")
})
}
return {state, submit}
추가 작업을 하지 않으면 새로고침시 로그인이 풀리게되는데
vuex 로 전역으로 상태를 관리하고,
sessionStorage 로 유저id를 세션으로 관리할 수 있습니다.
router.push 로 페이지 이동
vuex 로 로그인한 id 관리하기
scripts - store.js
import { createStore } from 'vuex'
// vuex : 전역으로 상태를 관리
const store = createStore({
state () {
return {
account:{
id: 0
}
}
},
mutations: {
setAccount(state, payload){
state.account.id = payload;
}
}
})
export default store;
main.js
import { createApp } from 'vue'
import App from './App.vue'
import store from '@/scripts/store'
import router from "@/scripts/router";
createApp(App).use(store).use(router).mount('#app')
use에 넣어두어서 해당 앱에서 자유롭게 사용
App.vue
<template>
<Header/>
<RouterView/>
<Footer/>
</template>
<script>
import Header from "@/components/Header"
import Footer from "@/components/Footer"
import store from "@/scripts/store";
import axios from "axios";
import {watch} from "vue";
import {useRoute} from "vue-router";
export default {
name: 'App',
components: {
Header, Footer
},
setup(){
const check = () =>{
axios.get("/api/account/check").then(({data})=>{
console.log(data);
store.commit("setAccount",data || 0);
})
};
const route = useRoute();
watch(route, ()=>{
check();
})
}
}
</script>
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
새로고침시 check api 를 통해
쿠키를 확인하여 로그인 유지
setup(){
const check = () =>{
axios.get("/api/account/check").then(({data})=>{
store.commit("setAccount",data || 0);
})
};
const route = useRoute();
watch(route, ()=>{
check();
})
}
벡엔드 check api
@GetMapping("/api/account/check")
public ResponseEntity check(@CookieValue(value = "token", required = false) String token){
Claims claims = jwtService.getClaims(token);
if(claims != null){
int id = Integer.parseInt(claims.get("id").toString());
return new ResponseEntity<>(id, HttpStatus.OK);
}
return new ResponseEntity<>(null, HttpStatus.OK);
}
헤더
Header.vue
<template>
<header>
<div class="collapse bg-dark" id="navbarHeader">
<div class="container">
<div class="row">
<div class="col-sm-4 py-4">
<h4 class="text-white">사이트맵</h4>
<ul class="list-unstyled">
<li>
<router-link to="/" class="text-white">메인 화면</router-link>
</li>
<li>
<router-link to="/login" class="text-white" v-if="!$store.state.account.id">로그인</router-link>
<a to="/login" class="text-white" @click="logout()" v-else>로그아웃</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="navbar navbar-dark bg-dark shadow-sm">
<div class="container">
<a href="/" class="navbar-brand d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor"
stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="me-2"
viewBox="0 0 24 24">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
<circle cx="12" cy="13" r="4"/>
</svg>
<strong>Album</strong>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarHeader"
aria-controls="navbarHeader" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
</header>
</template>
<script>
import store from "@/scripts/store";
import router from "@/scripts/router";
import axios from "axios";
export default {
name: 'Header',
setup() {
const logout = () => {
axios.get(('/api/account/logout')).then((res)=>{
if(res){
store.commit('setAccount', 0);
router.push({path: "/login"})
}
})
}
return {logout}
}
}
</script>
<style scoped>
</style>
v-if 조건으로 store의 id 가 없으면 "로그인",
v-else , 있으면 "로그아웃"
<router-link to="/login" class="text-white" v-if="!$store.state.account.id">로그인</router-link>
<a to="/login" class="text-white" @click="logout()" v-else>로그아웃</a>
로그아웃
setup() {
const logout = () => {
axios.get(('/api/account/logout')).then((res)=>{
if(res){
store.commit('setAccount', 0);
router.push({path: "/login"})
}
})
}
return {logout}
}
로그아웃은 store의 id 상태값을 0 으로 초기화,
벡엔드
@GetMapping("/api/account/logout")
public ResponseEntity<Boolean> logout(HttpServletRequest request, HttpServletResponse httpServletResponse){
Cookie cookie = new Cookie("token", "0"); // 변경할 쿠키의 이름과 값 설정
cookie.setMaxAge(0); // 쿠키의 유효기간 설정 (초단위, 예: 1시간 = 3600초)
cookie.setPath("/"); // 쿠키의 유효 경로 설정 (루트 경로인 경우 "/")
httpServletResponse.addCookie(cookie);
return ResponseEntity.ok(true);
}
쿠키를 다시 생성해서 0초 설정하여 없어지도록 함.
이슈해결
multi-word-component-names
package.json
"rules": {
"vue/multi-word-component-names": 0
}
한단어로도 가능하게 해결
마지막으로 CORS 문제
같은 IP에서 port 번호가 달라 서로 다른 출처에서 통신을 하기 때문에 설정을 따로 해둡니다
vue.config.js
module.exports = {
devServer: {
proxy: {
"/api": {
target: "http://localhost:8080",
changeOrigin: true,
pathRewrite:{
'^/': ''
}
},
},
}
}
'Front-end > Vue' 카테고리의 다른 글
Vue 3 쇼핑몰 만들기 - 2 (+ Spring Boot) (0) | 2023.04.20 |
---|---|
VUE 3 간단한 메모앱 실습 (0) | 2023.04.13 |
댓글