hands on vue3 - vue, spring boot, MySql rest-api로 초간단 게시판 만들기 프로젝트
[목표]
이전 포스팅에서 api 를 통하여
Vue.js 로 간단한 조회를 구현해보았다.
이번에는
게시판에서 제목, 내용을 검색하는
조건 있는 조회를 만들어보자.
이후에 게시판 작성도 포스팅 예정이다.
게시판 조회기능 구현은 다음 순서로 진행한다.
- MySQL에 테이블, 데이터 생성하기
- Spring Boot를 이용해 rest-api 조회 로직 만들기
- Vue.js(VSCODE)에서 제목/내용에 포함된 내용을 조회하여 테이블 그리기
1. MySQL에 테이블, 데이터 생성하기
CREATE TABLE board (
bid INT NOT NULL AUTO_INCREMENT,
target VARCHAR(20),
gubun VARCHAR(20),
title VARCHAR(50),
content VARCHAR(50),
hit VARCHAR(50),
PRIMARY KEY(bid)
);
INSERT INTO board(target, gubun, title, content, hit, regdate)
VALUES
('개인회원','안내', '주기적인 암호 변경 안내문', '주기적으로 암호를 변경해야 보안상 유리합니다. 모든 개인회원분들 께서는 암호를 변경하시길 바랍니다.', '5123214', '2023-01-10'),
('전체','안내', '음식물쓰레기 안내문', '건물 내에 있는 사원분들께서 음식물 쓰레기를 제대로 버리지 않습니다. 주의하시길 바랍니다.', '1021', '2023-02-10'),
('특별회원','프로모션', '특별회원분들께만 제공되는 특별 프로모션!', '차주 수요일부터 금요일까지 와인 할인 대행사가 시작됩니다.', '1023131', '2023-03-29'),
('개인회원','공지', '첫 가입 후 3개월은 매일 접속하시면 사은품을 드립니다.', '가입 후 3개월 동안 매일 접속하시면 사은품을 드려요.', '712631', '2023-01-30'),
('특별회원','프로모션', '1+1 프로모션 기간', '오늘부터 일주일간 애플 핸드폰 1+1 이벤트가 시작됩니다.', '124131', '2023-04-02'),
('개인회원','프로모션', '개인회원분들께 제공되는 프로모션! 놓치지 마세요', '점심시간 이후에 뽑기를 하시면 추첨을 통하여 영화티켓을 드립니다.', '9912312', '2022-12-10'),
('전체','공지', '주차안내문', '방문자들께서는 방문하시는 장소에서 주차증을 꼭 발급받으세요', '44212', '2022-11-12'),
('특별회원','공지', '라운지에서 식사 제공 공지', '저녁있는 수요일 밤마다 라운지에서 식사를 제공해드립니다. ', '12111', '2023-02-19'),
('개인회원','안내', '대리주차 서비스 종료 안내문', '아침 10시부터 오후 2시 사이에 제공되는 발렛 서비스가 곧 종료됩니다.', '423214', '2022-10-10')
;
2. Spring Boot를 이용해 rest-api 로직 만들기
먼저 프로젝트를 생성해보자
패키지를 셋팅해보자
com.board는 프로젝트 생성할 때
기본 패키지명이다.
꼭 com.board 하위 패키지에
새로운 패키지를 만들어야 한다.
왜냐하면 component-scan이
com.board 안에 있기 때문이다.
Config를 설정하자
config 설정 순서는 다음과 같다.
- Context 설정 (BoardProjectApplication.java)
- port, DB설정 (application.properties)
더 자세히 알고 싶다면 아래 더보기를 보자
DB데이터를 가져오는 흐름은
큰 틀로 보면 아래 사진과 같다.

출처 skplanet tacademy
DatabaseConfig.java는
Spring에서 dispatcher context이다.
Spring에서는 보통 xml로 설정을 많이한다.
예를 들어
web.xml, servlet.xml, root-context.xml
이런 파일명들에다가 설정을 했었다.
하지만
DatabaseConfig.java(스프링부트) 파일처럼
어노테이션을 선언하여 환경설정을 해주는 경우도 있다.
아무튼 우리가 헷갈리는 부분은
DAO의 @Mapper 어노테이션과
XML의 namespace가 연결되어야 하고..
DatabaseConfig.java 파일에는
패키지명을 또 알려줘야하고... 복잡하다..
그림을 보며 이해해보자

빨간 영역을 자세히 보자.
시간이 흘러 프로젝트가 조금 커지면
@Mapper로 선언된 파일과 다양한 mapper.xml 생길 것이다.
그렇다면
@Mapper는 어떤 XML이랑 연결되어 있을까?
서로 짝을 지어줘야한다.
mapper.xml의 namespace가 바로 그 연결고리다.
아래의 사진은 한 쌍이다.

또한 중요한 것이 있다.
MyBatis입장에서는
MyBatis 자신에게 요청하는 파일들이 어디에 있는지 미리 알고 있어야한다.
비유하자면
식당 가게 입구가 어딘지 알아야
손님이 오는지 안오는지 알지..
그것을 알려주는 환경파일이
DatabaseConfig.java다.


package com.board.confing;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@MapperScan(basePackages="com.board.DAO")
@EnableTransactionManagement
public class DatabaseConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setTypeAliasesPackage("com.board.DTO");
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sessionFactory.setMapperLocations(resolver.getResources("classpath:com/board/mapper/*.xml"));
return sessionFactory.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) throws Exception {
final SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
return sqlSessionTemplate;
}
}
Configuration의 의미는 다음 사진과 같다.
빨간 박스만 의미를 이해하려고 노력하고
나머지는 아~~ 그렇구나 하고 넘어가자
application.properties를 설정하자
server.port=8083
spring.datasource.url=jdbc:mysql://localhost:3306/vueprojectdb?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
DB연결할 때마다 헷갈리는데
테이블 명인지 스키마 명인지..
설정할 때는 스키마 이름으로 설정해준다.
DTO를 만들자
package com.board.DTO;
public class BoardDTO {
int bid;
String target;
String gubun;
String title;
String content;
int hit;
String regdate;
public int getBid() {
return bid;
}
public void setBid(int bid) {
this.bid = bid;
}
public String getTarget() {
return target;
}
public void setTarget(String target) {
this.target = target;
}
public String getGubun() {
return gubun;
}
public void setGubun(String gubun) {
this.gubun = gubun;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public int getHit() {
return hit;
}
public void setHit(int hit) {
this.hit = hit;
}
public String getRegdate() {
return regdate;
}
public void setRegdate(String regdate) {
this.regdate = regdate;
}
}
쿼리를 작성하자
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.board.DAO.BoardDAO">
<select id="getBoard" resultType="BoardDTO">
select * from board
WHERE 1=1
<if test="title != null">
AND title like concat('%', #{title}, '%')
</if>
<if test="content != null">
AND content like concat('%', #{content}, '%')
</if>
</select>
</mapper>
WHERE에 1=1을 하는 이유는
만약 1=1을 안한다면 다음과 같은 쿼리가 나타날 수도 있다
SELECT * FROM board
WHERE AND title like concat('%', #{title}, '%')
WHERE 뒤에
바로 AND가 온다면
쿼리문 에러다.
이를 방지하기 위해
1=1을 미리 붙여 놓는 것이다.
SELECT * FROM BOARD
WHERE 1=1
라고 치면 모든 값이 조회된다.
1=1은 TRUE이기 때문에
모든 조회가 된다.
쿼리를 통해
데이터를 가져오는
DAO를 작성하자
package com.board.DAO;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.board.DTO.BoardDTO;
@Mapper
public interface BoardDAO {
public List<BoardDTO> getBoard(BoardDTO bDTO);
}
조심해야할 부분은
interface로 생성해야된다는 점이다.
Service를 만들자
먼저 interface로
서비스를 만들자
인터페이스로 생성하는 것을 유의하자
package com.board.service;
import java.util.List;
import com.board.DTO.BoardDTO;
public interface BoardService {
public List<BoardDTO> getBoard(BoardDTO bDTO);
}
이제
서비스 인터페이스를
구현한 클래스를 만들자
package com.board.service;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.board.DAO.BoardDAO;
import com.board.DTO.BoardDTO;
@Service
public class BoardServiceImpl implements BoardService{
@Autowired
BoardDAO bDao;
@Override
public List<BoardDTO> getBoard(BoardDTO bDTO) {
List<BoardDTO> list = bDao.getBoard(bDTO);
return list;
}
}
Controller를 만들자
package com.board.controller;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.board.DTO.BoardDTO;
import com.board.service.BoardService;
@RestController
public class BoardController {
private final Logger log = LoggerFactory.getLogger(this.getClass().getSimpleName());
@Autowired
BoardService bService;
@RequestMapping(value="/getBoard", method=RequestMethod.POST)
public List<BoardDTO> getBoard(@RequestBody BoardDTO bDTO){
log.info("In BoardController");
log.info(bDTO.getContent());
log.info(bDTO.getTitle());
List<BoardDTO> list = bService.getBoard(bDTO);
return list;
}
}
자!
이제 Spring boot에서는
모든 셋팅이 끝났다.
VsCode로
Vue.js를 이용하여 테이블을 구현하자.

3.Vue.js(VSCODE)에서 제목/내용에 포함된 내용을 조회하여 테이블 그리기
Vue.js 로 만들 순서는 다음과 같다.
- 프로젝트 생성하기
- 프록시설정, 라우터 셋팅
- 테이블 만들기
프로젝트를 생성하자
기본 셋팅 되어 있는
Hello World.vue가 필요없으니
깨끗하게 없애자
전 포스팅에서는
일부로 CORS 오류를 만나면서
프록시 필요성에 대해 알아봤었는데
이번 포스팅에서는
그냥 바로 프록시 설정을 하겠다.
궁금한 사람들은
아래 포스팅을 통해 확인하거나
구글링 하시면 된다.
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
//8083은 springboot에서 설정한 포트이다.
//사람마다 다를 수 있으니 확인하고 설정하시길 바란다.
proxy: 'http://localhost:8083'
},
})
이번에는
라우터 설정을 해보자
먼저 CMD창에
npm install vue-router
명령을 통해 설치한 뒤에
main.js에서
router 셋팅을 해보자
import { createApp } from 'vue'
import App from './App.vue'
import {createRouter, createWebHistory} from 'vue-router'
const app = createApp(App);
const router = createRouter({
history: createWebHistory(),
routes: [{
path: '/*',
component: App
}]
});
app.use(router);
app.mount('#app')
기본 설정 셋팅 끝!

CSS를 잘 모르기 때문에
라이브러리인
부트스트랩으로
진행할 예정이다.
따라하실 분들은
사이트에서
CDN으로 가져올 수 있는 코드를
index.html에
복붙하면 된다.
그냥 다음 소스를
index.html에다가 복붙하자... 귀찮으니까^^
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<!-- 부트스트랩 가져오기 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
<!-- 부트스트랩가져오기 끝 -->
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
제목/내용 option을 선택하여 키워드를 입력하고 검색을 클릭하면 api 요청해보자
<template>
<div>
<select v-model="searchType">
<option value="title">제목</option>
<option value="content">내용</option>
</select>
<input type="text" v-model="searchWord">
<button type="button" class="btn btn-outline-primary" @click="goSearch">검색</button>
</div>
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">대상</th>
<th scope="col">구분</th>
<th scope="col">제목</th>
<th scope="col">내용</th>
<th scope="col">등록일자</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr v-for="item in boardList" :key="item.bid">
<th scope="row">{{item.bid}}</th>
<td>{{item.target}}</td>
<td>{{item.gubun}}</td>
<td>{{item.title}}</td>
<td>{{item.content}}</td>
<td>{{item.regdate}}</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
data(){
return{
//기본값 title로 합니다.
searchType:'title',
title: '',
content: '',
boardList: [],
searchWord: '',
}
},
methods: {
goSearch(){
if(this.searchType == 'content')
this.content = this.searchWord;
if(this.searchType != 'content')
this.title = this.searchWord;
let param ={
'title' : this.title,
'content' : this.content,
};
console.log(JSON.stringify(param));
fetch('/getBoard', {
method: 'POST', // 또는 'PUT'
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(param),
})
.then((response) => response.json())
.then((data) => {
//데이터를 성공적으로 받았을 때
console.log('성공:', data);
this.boardList = data;
console.log(data[0]);
//param으로 보내준거 비워주기
this.content = '';
this.title = '';
})
.catch((error) => {
//데이터를 못 받았을 때
console.error('실패:', error);
});
}
}
}
</script>
<style scoped>
div{
margin:auto;
padding:10px;
position:relative;
left: 70%;
}
input{
margin:10px;
}
</style>
내용을 선택하고
유리 라는 키워드를 입력후
검색을 클릭한 결과다.
도움이 되었으면 좋겠습니다.