데일리 공부 기록

hands on vue3 - vue, spring boot, MySql rest-api로 초간단 게시판 만들기 프로젝트

탐훈 2023. 4. 2. 20:18
728x90

[목표]

이전 포스팅에서 api 를 통하여 

Vue.js 로 간단한 조회를 구현해보았다.

 

이번에는 

게시판에서 제목, 내용을 검색하는 

조건 있는 조회를 만들어보자.

 

이후에 게시판 작성도 포스팅 예정이다. 


게시판 조회기능 구현은 다음 순서로 진행한다. 

  1. MySQL에 테이블, 데이터 생성하기
  2. Spring Boot를 이용해 rest-api 조회 로직 만들기
  3. 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 설정 순서는 다음과 같다.

  1. Context 설정 (BoardProjectApplication.java)
  2. 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 로 만들 순서는 다음과 같다. 

 

  1. 프로젝트 생성하기
  2. 프록시설정, 라우터 셋팅
  3. 테이블 만들기

 

 


프로젝트를 생성하자

 

 

기본 셋팅 되어 있는

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>

 

내용을 선택하고

유리 라는 키워드를 입력후

검색을 클릭한 결과다.

 

 

 


 

도움이 되었으면 좋겠습니다.