
버전(2.7.17) 변경, mybatis-spring-boot-starter(2.3.1) 버전 변경, mybatis-spring-boot-starter-test(2.3.1) 버전 변경, 라이브러리 추가
pom.xml에서 오류가 발생한다면 maven update 실행
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.17</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>2.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
계층적인 형태로 작성하는 것이 더 편하기 때문에 yml 코드를 작성함


spring:
profiles: #실행환경(local(로컬),dev(개발),real(운영))
active: local
mvc:
view: #view 경로 및 확장자 지정 (우리의 view 코드는 views 폴더에 포함이 되어있고, 뷰는 jsp로 항상 입력)
prefix: /WEB-INF/views/
suffix: .jsp
datasource: #DB 접속
driver-class-name: oracle.jdbc.OracleDriver
url: jdbc:oracle:thin:@localhost:1521:xe
username: user01
password: 1234
hikari: #기본 커넥션풀
connection-timeout : 30000 #클라이언트가 pool에 connection을 요청하는데 기다리는 최대시간을 설정 30초
maximum-pool-size : 10 #최대 커넥션 개수 설정
max-lifetime: 1800000 #커넥션 풀에서 살아있을 수 있는 커넥션의 최대 수명시간 1,800초(30분)
messages: #메시지(파일이 여러개일 경우 ,로 구분) (에러 메세지 처리하는 곳)
basename: messages.validation
encoding: UTF-8
devtools: #View reload (클래스, jsp 자동으로 reload 되도록 설정 바로 바로 적용이 되도록)
livereload:
enabled: true
restart: #컨트롤러, 모델단이 바뀌었을 때 프로젝트 재시작 설정 (정확히는 classpath(src)에 있는 모든 파일)
enabled: true
mybatis: #mybatis 설정
type-aliases-package: kr.spring.**.vo # 폴더가 변경이 가능하다는 의미
mapper-locations: kr/spring/**/dao/*.xml # dao에 mapper를 위치하겠다
#mapper-locations: mybatis/mapper/**/*.xml
logging: #로그 지정
level:
#root: error
'[kr.spring]': debug
server:
port: 8000
servlet:
#context-path: /mybatis
encoding:
charset: UTF-8
enabled: true #http 인코딩 지원을 활성화할지 여부
force: true
application properties 를 사용하고 싶다면 해당 코드를 작성하면 된다
#tomcat port setting
server.port=8000
#JSP setting (main 밑에 webapp/WEB-INF/views 폴더 생성 후)
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
#oracle setting
spring.datasource.hikari.driver-class-name=oracle.jdbc.OracleDriver
spring.datasource.hikari.jdbc-url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.hikari.username=scott
spring.datasource.hikari.password=tiger
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
#클라이언트가 pool에 connection을 요청하는데 기다리는 최대시간을 설정 30초
#설정한 시간을 초과하면 SQLException이 발생
spring.datasource.hikari.connection-timeout=30000
#connection pool에서 유지가능한 최소 커넥션 개수를 설정합니다.
#최적의 성능과 응답성을 원하면 이 값을 설정하지 않는게 좋다고 함
#spring.datasource.hikari.minimum-idle=5
#connection의 최대 유지 시간을 설정합니다.
#connection의 maxLifeTime 지났을 때, 사용중인 connection은 바로 폐기되지않고 작업이 완료되면 폐기됩니다.
#하지만 유휴 커넥션은 바로 폐기됩니다.
spring.datasource.hikari.max-lifetime=240000
#유휴 및 사용중인 connection을 포함하여 풀에 보관가능한 최대 커넥션 개수를 설정
#사용할 수 있는 커넥션이 없다면 connectionTimeout 시간 만큼 대기하고 시간을 초과하면 SQLException이 발생
spring.datasource.hikari.maximum-pool-size=20
#spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
#spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
#spring.datasource.username=scott
#spring.datasource.password=tiger
spring.messages.basename=messages.validation
#JSP reload
spring.devtools.livereload.enabled=true
#mybatis setting
mybatis.mapper-locations=classpath:mybatis/mapper/**/*.xml
mybatis.type-aliases-package=kr.spring.**.vo
#log setting(default->info)
logging.level.kr.spring=DEBUG
server.servlet.context-path:/
index.jsp는 실행이 불가능하다. MVC 패턴일때만 index.jsp를 통하여 호출
지금은 새로운 방식으로 설정을 할 예정
package kr.spring.board.vo;
import java.sql.Date;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
public class BoardVO {
private int num;
@NotBlank
private String writer;
@NotBlank
private String title;
@NotBlank
private String passwd;
@NotEmpty
private String content;
private Date reg_date;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getWriter() {
return writer;
}
public void setWriter(String writer) {
this.writer = writer;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Date getReg_date() {
return reg_date;
}
public void setReg_date(Date reg_date) {
this.reg_date = reg_date;
}
@Override
public String toString() {
return "BoardVO [num=" + num + ", writer=" + writer + ", title=" + title + ", passwd=" + passwd + ", content="
+ content + ", reg_date=" + reg_date + "]";
}
}
Interface 파일
@Mapper 어노테이션을 작성하지 않으면 자동으로 생성하지 못하기 때문에 꼭 작성해줘야 한다.
package kr.spring.board.dao;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import kr.spring.board.vo.BoardVO;
@Mapper
public interface BoardMapper {
@Insert("INSERT INTO aboard VALUES(aboard_seq.nextval,#{writer},#{title},#{passwd},#{content},SYSDATE)")
public void insertBoard(BoardVO board);
@Select("SELECT COUNT(*) FROM aboard")
public int getBoardCount();
public List<BoardVO> getBoardList(Map<String, Integer> map);
@Select("SELECT * FROM aboard WHERE num=#{num}")
public BoardVO selectBoard(int num);
@Update("UPDATE aboard SET writer=#{writer}, title=#{title}, content=#{content} WHERE num=#{num}")
public void updateBoard(BoardVO board);
@Delete("DELETE FROM aboard WHERE num=#{num}")
public void deleteBoard(int num);
}
<?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">
<!-- interface명칭과 xml의 명칭이 동일해야 한다 -->
<mapper namespace="kr.spring.board.dao.BoardMapper">
<!-- 목록 가져오기 , 아이디는 따로 명시하지 않았음 그러나 메소드 명을 아이디로 사용 가능함-->
<select id="getBoardList" parameterType="map" resultType="boardVO">
SELECT * FROM(SELECT a.*,rownum rnum FROM (SELECT * FROM aboard ORDER BY num DESC)a)
<![CDATA[
WHERE rnum >= #{start} AND rnum <= #{end}
]]>
</select>
</mapper>
package kr.spring.board.service;
import java.util.List;
import java.util.Map;
import kr.spring.board.vo.BoardVO;
public interface BoardService {
public void insertBoard(BoardVO board);
public int getBoardCount();
public List<BoardVO> getBoardList(Map<String, Integer> map);
public BoardVO selectBoard(int num);
public void updateBoard(BoardVO board);
public void deleteBoard(int num);
}
package kr.spring.board.service;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import kr.spring.board.dao.BoardMapper;
import kr.spring.board.vo.BoardVO;
@Service
@Transactional
public class BoardServiceImpl implements BoardService{
@Autowired
private BoardMapper boardMapper;
@Override
public void insertBoard(BoardVO board) {
boardMapper.insertBoard(board);
}
@Override
public int getBoardCount() {
return boardMapper.getBoardCount();
}
@Override
public List<BoardVO> getBoardList(Map<String, Integer> map) {
return boardMapper.getBoardList(map);
}
@Override
public BoardVO selectBoard(int num) {
return boardMapper.selectBoard(num);
}
@Override
public void updateBoard(BoardVO board) {
boardMapper.updateBoard(board);
}
@Override
public void deleteBoard(int num) {
boardMapper.deleteBoard(num);
}
}
@SpringBootApplication
이 어노테이션은 여러 어노테이션을 합친 것으로, @Configuration, @EnableAutoConfiguration,@ComponentScan을 포함한다. 이 어노테이션이 있으면 스프링 부트는 애플리케이션 설정을 자동으로 구성하고, 컴포넌트를 스캔하여 필요한 빈(bean)을 자동으로 등록한다.
이 메소드는 자바 애플리케이션의 시작 지점이다.
SpringApplication.run(Ch12SpringMybatisBootApplication.class, args);
스프링 부트 애플리케이션을 시작하고, 내장된 서버(톰캣)를 실행합니다.
package kr.spring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Ch12SpringMybatisBootApplication {
public static void main(String[] args) {
SpringApplication.run(Ch12SpringMybatisBootApplication.class, args);
}
}
SpringApplication.run 메소드는 스프링 부트 애플리케이션을 초기화하고 실행@SpringBootApplication 어노테이션 덕분에 스프링 부트는 설정, 빈 등록 등을 자동으로 처리된다.@Controller, @RestController, @Service, @Repository 등의 어노테이션이 붙은 클래스를 스캔하고 관리한다.@RequestMapping("/")은 웹 애플리케이션의 시작점을 설정하는 역할이기 때문에 Ch12SpringMyBatisBootApplication.java의 마지막 부분인 url 요청의 역할을 한다고 보면 된다. 그래서 해당 프로젝트를 실행시키면 list.do가 실행되는 것이다.
@RequestMapping("/")
public String main() {
return "redirect:/list.do";
}
package kr.spring.board.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import kr.spring.board.service.BoardService;
import kr.spring.board.vo.BoardVO;
import kr.spring.util.PagingUtil;
@Controller
public class BoardController {
@Autowired
private BoardService boardService;
//로그 처리(로그 대상 지정)
private static final Logger log =
LoggerFactory.getLogger(BoardController.class);
//유효성 체크를 위한 자바빈(VO) 초기화
@ModelAttribute
public BoardVO initCommand(){
return new BoardVO();
}
// 초기 진입에 대한 요청 넣기
@RequestMapping("/")
public String main() {
return "redirect:/list.do";
}
//글쓰기 폼 호출
@GetMapping("/insert.do")
public String form() {
return "insertForm";
}
//전송된 데이터 처리
@PostMapping("/insert.do")
public String submit(@Valid BoardVO vo,
BindingResult result) {
log.debug("<<BoardVO>> : " + vo);
//유효성 체크 결과 오류가 있으면 폼 호출
if(result.hasErrors()) {
return form();
}
//글쓰기
boardService.insertBoard(vo);
return "redirect:/list.do";
}
//목록
@RequestMapping("/list.do")
public ModelAndView getList(
@RequestParam(value="pageNum",
defaultValue="1") int currentPage) {
//총레코드 수
int count = boardService.getBoardCount();
//페이지 처리
PagingUtil page =
new PagingUtil(currentPage,count,20,10,"list.do");
//목록 호출
List<BoardVO> list = null;
if(count > 0) {
Map<String,Integer> map =
new HashMap<String,Integer>();
map.put("start", page.getStartRow());
map.put("end", page.getEndRow());
list = boardService.getBoardList(map);
}
ModelAndView mav = new ModelAndView();
//뷰 이름 설정
mav.setViewName("selectList");
//데이터 저장
mav.addObject("count", count);
mav.addObject("list", list);
mav.addObject("page", page.getPage());
return mav;
}
//글상세
@RequestMapping("/detail.do")
public ModelAndView detail(int num) {
BoardVO board =
boardService.selectBoard(num);
return new ModelAndView("selectDetail","board",board);
}
//수정 폼 호출
@GetMapping("/update.do")
public String formUpdate(int num,Model model) {
model.addAttribute("boardVO",
boardService.selectBoard(num));
return "updateForm";
}
//전송된 데이터 처리
@PostMapping("/update.do")
public String submitUpdate(@Valid BoardVO vo,
BindingResult result) {
log.debug("<<BoardVO>> : " + vo);
//유효성 체크 결과 오류가 있으면 폼 호출
if(result.hasErrors()) {
return "updateForm";
}
//비밀번호 일치 여부 체크
//DB에 저장된 비밀번호 구하기
BoardVO db_board =
boardService.selectBoard(vo.getNum());
//비밀번호 체크
if(!db_board.getPasswd().equals(vo.getPasswd())) {
result.rejectValue("passwd", "invalidPassword");
return "updateForm";
}
//글 수정
boardService.updateBoard(vo);
return "redirect:/list.do";
}
//글 삭제 폼 호출
@GetMapping("/delete.do")
public String formDelete(BoardVO vo) {
return "deleteForm";
}
//글 삭제 처리
@PostMapping("/delete.do")
public String submitDelete(@Valid BoardVO vo,
BindingResult result) {
log.debug("<<BoardVO>> : " + vo);
//유효성 체크 결과 오류가 있으면 폼 호출
//비밀번호 전송 여부만 체크
if(result.hasFieldErrors("passwd")) {
return "deleteForm";
}
//비밀번호 일치 여부 체크
//DB에 저장된 비밀번호 구하기
BoardVO db_board =
boardService.selectBoard(vo.getNum());
//비밀번호 체크
if(!db_board.getPasswd().equals(vo.getPasswd())) {
result.rejectValue("passwd", "invalidPassword");
return "deleteForm";
}
//글 삭제
boardService.deleteBoard(vo.getNum());
return "redirect:/list.do";
}
}


버전(2.7.17) 변경, mybatis-spring-boot-starter(2.3.1) 버전 변경, mybatis-spring-boot-starter-test(2.3.1) 버전 변경, 라이브러리 추가
pom.xml에서 오류가 발생한다면 maven update 실행
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.17</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>2.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tiles</groupId>
<artifactId>tiles-servlet</artifactId>
<version>3.0.7</version>
</dependency>
<dependency>
<groupId>org.apache.tiles</groupId>
<artifactId>tiles-jsp</artifactId>
<version>3.0.7</version>
</dependency>
계층적인 형태로 작성하는 것이 더 편하기 때문에 yml 코드를 작성함


spring:
profiles: #실행환경(local(로컬),dev(개발),real(운영))
active: local
mvc:
view: #view 경로 및 확장자 지정 (우리의 view 코드는 views 폴더에 포함이 되어있고, 뷰는 jsp로 항상 입력)
prefix: /WEB-INF/views/
suffix: .jsp
datasource: #DB 접속
driver-class-name: oracle.jdbc.OracleDriver
url: jdbc:oracle:thin:@localhost:1521:xe
username: user01
password: 1234
hikari: #기본 커넥션풀
connection-timeout : 30000 #클라이언트가 pool에 connection을 요청하는데 기다리는 최대시간을 설정 30초
maximum-pool-size : 10 #최대 커넥션 개수 설정
max-lifetime: 1800000 #커넥션 풀에서 살아있을 수 있는 커넥션의 최대 수명시간 1,800초(30분)
messages: #메시지(파일이 여러개일 경우 ,로 구분) (에러 메세지 처리하는 곳)
basename: messages.validation
encoding: UTF-8
devtools: #View reload (클래스, jsp 자동으로 reload 되도록 설정 바로 바로 적용이 되도록)
livereload:
enabled: true
restart: #컨트롤러, 모델단이 바뀌었을 때 프로젝트 재시작 설정 (정확히는 classpath(src)에 있는 모든 파일)
enabled: true
mybatis: #mybatis 설정
type-aliases-package: kr.spring.**.vo # 폴더가 변경이 가능하다는 의미
mapper-locations: kr/spring/**/dao/*.xml # dao에 mapper를 위치하겠다
#mapper-locations: mybatis/mapper/**/*.xml
logging: #로그 지정
level:
#root: error
'[kr.spring]': debug
server:
port: 8000
servlet:
#context-path: /mybatis
encoding:
charset: UTF-8
enabled: true #http 인코딩 지원을 활성화할지 여부
force: true
Spring Framework에서 Tiles를 사용하여 View를 구성하는 방법을 설정하는 Java 설정 파일
@Configuration
- 해당하는 클래스가 Spring의 Java 기반 설정 파일임을 나타내는 어노테이션
public class AppConfig implements WebMvcConfigurer
- WebMvcConfigurer 인터페이스를 구현한 클래스
- 이를 통해 Spring MVC의 구성을 추가적으로 설정
@Bean
- 메서드 레벨에서 해당 애너테이션이 사용되면, 해당 메서드가 Spring 컨테이너에 의해 관리되는 빈(Bean)을 정의한다는 것을 나타냄
TilesConfigurer tilesConfigurer()
- TilesConfigurer 객체를 생성하고 반환하는 메서드
- TilesConfigurer 는 Tiles 설정을 관리하는 데 사용된다.
setDefinitions(new String[] {"/WEB-INF/tiles-def/tilesdef.xml"})
- Tiles의 정의 파일 경로를 설정한다.
- /WEB-INF/tiles-def/tilesdef.xml 파일에는 Tiles의 레이아웃 및 타일 정의가 포함되어 있어야 한다.
setCheckRefresh(true)
- 설정 파일을 변경할 때마다 Tiles 설정을 새로 고침할지 여부를 설정
TilesViewResolver tilesViewResolver()
- TilesViewResolver 객체를 생성하고 반환하는 메서드
- 이 Resolver는 Tiles 뷰를 해석하여 반환
setViewClass(TilesView.class)
- 이 Resolver가 사용할 뷰 클래스를 TilesView로 설정한다.
- 이 설정 파일을 사용하면 Spring MVC 애플리케이션에서 Tiles를 통해 레이아웃 기반의 View 구성을 쉽게 할 수 있습니다.
- /WEB-INF/tiles-def/tilesdef.xml 파일에는 Tiles의 정의가 포함되어 있어야 하며, 이 파일에서는 각 타일의 레이아웃과 실제 뷰를 매핑할 수 있다.
package kr.spring.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
import org.springframework.web.servlet.view.tiles3.TilesView;
import org.springframework.web.servlet.view.tiles3.TilesViewResolver;
@Configuration
public class AppConfig implements WebMvcConfigurer{
@Bean
public TilesConfigurer tilesConfigurer() {
final TilesConfigurer configurer = new TilesConfigurer();
// 해당 경로에 tiles.xml 파일을 넣음
configurer.setDefinitions(new String[] {"/WEB-INF/tiles-def/tilesdef.xml"});
configurer.setCheckRefresh(true);
return configurer;
}
@Bean
public TilesViewResolver tilesViewResolver() {
final TilesViewResolver tilesViewResolver = new TilesViewResolver();
tilesViewResolver.setViewClass(TilesView.class);
return tilesViewResolver;
}
}
main을 설정하는 기본 레이아웃과 title, body 외에는 모두 동일하기 때문에 main에서 바뀌지 않는 설정들을 상속을 받고 바뀌는 부분들만 작성해주면 된다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE tiles-definitions PUBLIC
"-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
"http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
<!-- 기본 레이아웃 설정 -->
<definition name="main" template="/WEB-INF/views/template/layout.jsp">
<put-attribute name="title" value="Spring Tiles Example"/>
<put-attribute name="header" value="/WEB-INF/views/template/header.jsp" />
<put-attribute name="menu" value="/WEB-INF/views/template/menu.jsp" />
<put-attribute name="body" value="/WEB-INF/views/main_view.jsp" />
<put-attribute name="footer" value="/WEB-INF/views/template/footer.jsp" />
</definition>
<!-- 기존 설정을 그대로 유지하는데 바뀌는 것만 명시 -->
<definition name="company" extends="main">
<put-attribute name="title" value="Company Introduction"/>
<put-attribute name="body" value="/WEB-INF/views/company_view.jsp" />
</definition>
<definition name="product" extends="main">
<put-attribute name="title" value="Product Introduction"/>
<put-attribute name="body" value="/WEB-INF/views/product_view.jsp" />
</definition>
</tiles-definitions>
footer에 위치하는 코드
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<div align="center" style="background: yellow;">
© Spring Tiles Example
</div>
header에 위치하는 코드
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<h2 align="center">Spring Tiles Example!</h2>
menu에 위치하는 코드
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<ul>
<li><a href="main.do">main</a></li>
<li><a href="company.do">회사 소개</a></li>
<li><a href="product.do">제품 소개</a></li>
</ul>
header, menu, body, footer를 모두 모아주는 코드
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title> <tiles:insertAttribute name="title" /> </title>
</head>
<body>
<table style="width: 100%;">
<tr height="100" valign="middle" bgcolor="#ffffff">
<td colspan="2"><tiles:insertAttribute name="header"/></td>
</tr>
<tr height="570">
<td width="15%" valign="top">
<tiles:insertAttribute name="menu"/>
</td>
<td width="85%" align="center">
<tiles:insertAttribute name="body"/>
</td>
</tr>
<tr>
<td colspan="2"><tiles:insertAttribute name="footer"/></td>
</tr>
</table>
</body>
</html>
body에 들어오는 내용은 각 페이지마다 다르기 때문에 template 안에 넣지 않고 작성한다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<b>Main Page Body</b>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<b>Company Introduction</b>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<b>Product Introduction</b>
ch13SpringTiles를 실행시켰을 때 처음으로 뜨는 화면을 지정해주는 코드이다.
여기서는 main.do를 실행하도록 return에서 페이지를 정해준다.
@RequestMapping("/")
public String init() {
return "redirect:/main.do";
}
main이 실행되도록 @RequestMapping("/main.do")에 해당하는 코드를 작성해준다.
return "main"; 해당 코드의 main은 tilesdef.xml에서 definition 태그의 name의 명칭과 동일해야 한다.
@RequestMapping("/main.do")
public String main() {
return "main";
}
company.do 호출하는 코드
return "company"; 해당 코드의 company는 tilesdef.xml에서 definition 태그의 name 명칭과 동일
@RequestMapping("/company.do")
public String viewCompany() {
return "company";
}
product.do 호출하는 코드
return "product"; 해당 코드의 product tilesdef.xml에서 definition 태그의 name 명칭과 동일
@RequestMapping("/product.do")
public String viewProduct() {
return "product";
}
package kr.spring.tiles.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class TilesController {
@RequestMapping("/")
public String init() {
return "redirect:/main.do";
}
@RequestMapping("/main.do")
public String viewMain() {
return "main";
}
@RequestMapping("/company.do")
public String viewCompany() {
return "company";
}
@RequestMapping("/product.do")
public String viewProduct() {
return "product";
}
}




버전(2.7.17) 변경, mybatis-spring-boot-starter(2.3.1) 버전 변경, mybatis-spring-boot-starter-test(2.3.1) 버전 변경
pom.xml에서 오류가 발생한다면 maven update 실행
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.17</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>2.3.1</version>
<scope>test</scope>
</dependency>
jsp는 더 이상 사용하지 않기 때문에 삭제해주고 타임리프 설정을 추가해주면 된다.
spring:
profiles: #실행환경(local(로컬),dev(개발),real(운영))
active: local
datasource: #DB 접속
driver-class-name: oracle.jdbc.OracleDriver
url: jdbc:oracle:thin:@localhost:1521:xe
username: user01
password: 1234
hikari: #기본 커넥션풀
connection-timeout : 30000 #클라이언트가 pool에 connection을 요청하는데 기다리는 최대시간을 설정 30초
maximum-pool-size : 10 #최대 커넥션 개수 설정
max-lifetime: 1800000 #커넥션 풀에서 살아있을 수 있는 커넥션의 최대 수명시간 1,800초(30분)
messages: #메시지(파일이 여러개일 경우 ,로 구분) (에러 메세지 처리하는 곳)
basename: messages.validation
encoding: UTF-8
devtools: #View reload (클래스, jsp 자동으로 reload 되도록 설정 바로 바로 적용이 되도록)
livereload:
enabled: true
restart: #컨트롤러, 모델단이 바뀌었을 때 프로젝트 재시작 설정 (정확히는 classpath(src)에 있는 모든 파일)
enabled: true
thymeleaf: #타임리프 설정
prefix: classpath:templates/
suffix: .html
check-template-location: true
enabled: true
mode: HTML5
cache: false #캐시는 사용하지 않음(새로고침시 변경사항 바로 반영)
mybatis: #mybatis 설정
type-aliases-package: kr.spring.**.vo # 폴더가 변경이 가능하다는 의미
mapper-locations: kr/spring/**/dao/*.xml # dao에 mapper를 위치하겠다
#mapper-locations: mybatis/mapper/**/*.xml
logging: #로그 지정
level:
#root: error
'[kr.spring]': debug
server:
port: 8000
servlet:
#context-path: /mybatis
encoding:
charset: UTF-8
enabled: true #http 인코딩 지원을 활성화할지 여부
force: true
package kr.spring.board.vo;
import java.sql.Date;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
public class BoardVO {
private int num;
@NotBlank
private String title;
@NotBlank
private String writer;
@NotEmpty
private String content;
@NotBlank
private String passwd;
private Date reg_date;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getWriter() {
return writer;
}
public void setWriter(String writer) {
this.writer = writer;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
public Date getReg_date() {
return reg_date;
}
public void setReg_date(Date reg_date) {
this.reg_date = reg_date;
}
@Override
public String toString() {
return "BoardVO [num=" + num + ", title=" + title + ", writer=" + writer + ", content=" + content + ", passwd="
+ passwd + ", reg_date=" + reg_date + "]";
}
}
package kr.spring.board.dao;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import kr.spring.board.vo.BoardVO;
@Mapper
public interface BoardMapper {
@Insert("INSERT INTO aboard VALUES(aboard_seq.nextval,#{writer},#{title},#{passwd},#{content},SYSDATE)")
public void insertBoard(BoardVO board);
@Select("SELECT COUNT(*) FROM aboard")
public int getBoardCount();
public List<BoardVO> getBoardList(Map<String, Integer> map);
@Select("SELECT * FROM aboard WHERE num=#{num}")
public BoardVO selectBoard(int num);
@Update("UPDATE aboard SET writer=#{writer}, title=#{title}, content=#{content} WHERE num=#{num}")
public void updateBoard(BoardVO board);
@Delete("DELETE FROM aboard WHERE num=#{num}")
public void deleteBoard(int num);
}
<?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">
<!-- interface명칭과 xml의 명칭이 동일해야 한다 -->
<mapper namespace="kr.spring.board.dao.BoardMapper">
<!-- 목록 가져오기 , 아이디는 따로 명시하지 않았음 그러나 메소드 명을 아이디로 사용 가능함-->
<select id="getBoardList" parameterType="map" resultType="boardVO">
SELECT * FROM(SELECT a.*,rownum rnum FROM (SELECT * FROM aboard ORDER BY num DESC)a)
<![CDATA[
WHERE rnum >= #{start} AND rnum <= #{end}
]]>
</select>
</mapper>
package kr.spring.board.service;
import java.util.List;
import java.util.Map;
import kr.spring.board.vo.BoardVO;
public interface BoardService {
public void insertBoard(BoardVO board);
public int getBoardCount();
public List<BoardVO> getBoardList(Map<String, Integer> map);
public BoardVO selectBoard(int num);
public void updateBoard(BoardVO board);
public void deleteBoard(int num);
}
package kr.spring.board.service;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import kr.spring.board.dao.BoardMapper;
import kr.spring.board.vo.BoardVO;
@Service
@Transactional
public class BoardServiceImpl implements BoardService{
@Autowired
private BoardMapper boardMapper;
@Override
public void insertBoard(BoardVO board) {
boardMapper.insertBoard(board);
}
@Override
public int getBoardCount() {
return boardMapper.getBoardCount();
}
@Override
public List<BoardVO> getBoardList(Map<String, Integer> map) {
return boardMapper.getBoardList(map);
}
@Override
public BoardVO selectBoard(int num) {
return boardMapper.selectBoard(num);
}
@Override
public void updateBoard(BoardVO board) {
boardMapper.updateBoard(board);
}
@Override
public void deleteBoard(int num) {
boardMapper.deleteBoard(num);
}
}
타임리프는 태그를 보여줄 것인가 안 보여줄 것인가로 나타냄
jstl은 태그를 지원, 타임리프는 속성을 지원한다고 보면 된다.
타임리프 속성을 이용해서 css 가져오기 th: 접두사
@{/} => ${pageContext.request.contextPath} 같은 의미를 나타냄
<link rel="stylesheet" th:href="@{/css/style.css}" type="text/css">
JSTL c:forEach 대신 타임리프 th:each 사용
board 변수를 할당하고 list를 가져오는 것임
<tr th:each="board : ${list}">
th:utext는 타임리프에서 HTML 콘텐츠를 렌더링할 때 사용한다.
<div class="align-center" th:utext="${page}"></div>
th:href 링크의 href 속성을 동적으로 설정
th:if 특정 조건에 따라 HTML 요소를 렌더링할지 여부를 결정
th:each 컬렉션을 반복하여 HTML 요소를 렌더링
th:text 요소의 텍스트 내용을 동적으로 설정
th:utext 텍스트 내용을 HTML로 해석하여 렌더링
@{/} 경로 앞에 슬래시(/)가 포함된 경우, 이는 컨텍스트 루트(context root)에서부터 경로를 지정하는 것을 의미함
@{} 슬래시(/) 없이 단순히 변수나 경로를 지정할 때 사용함
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>게시판 목록</title>
<link rel="stylesheet" th:href="@{/css/style.css}" type="text/css">
</head>
<body>
<div class="page-main">
<h2>게시판 목록</h2>
<div class="align-right">
<input type="button" value="글쓰기" onclick="location.href='insert.do'">
</div>
<div class="result-display" th:if="${count == 0}">표시할 내용이 없습니다.</div>
<div th:if="${count > 0}">
<table>
<tr>
<th>번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
</tr>
<tr th:each="board : ${list}">
<td th:text="${board.num}"></td>
<td><a th:href="@{detail.do(num=${board.num})}" th:text="${board.title}"></a></td>
<td th:text="${board.writer}"></td>
<td th:text="${board.reg_date}"></td>
</tr>
</table>
</div>
<div class="align-center" th:utext="${page}"></div>
</div>
</body>
</html>
th:object 는 request에 저장되어 있는 자바빈을 호출한다.
th:field 는 자바빈의 필드를 지정한다
th:for 속성은 해당 label 태그가 어떤 input 필드를 참조하는지 명시
*{writer} 는 Thymeleaf의 표현식으로, writer 필드를 의미
th:field="*{writer}" 는 input 필드의 값을 writer 필드와 바인딩하도록 한다. 이를 통해 폼의 데이터와 객체의 필드를 연결할 수 있다.
th:errorclass="field-error" 는 해당 input 필드에 유효성 검사 에러가 발생했을 때 추가할 클래스를 지정합니다. 여기서는 field-error 클래스를 추가하게 됩니다.
th:errors="*{writer}" 는 유효성 검사 에러 메시지를 출력하는 Thymeleaf의 기능
<label th:for="*{writer}">작성자</label>
<input type="text" th:field="*{writer}" th:errorclass="field-error">
<span th:errors="*{writer}" class="error-color"></span>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>글 작성하기</title>
<link rel="stylesheet" th:href="@{/css/style.css}" type="text/css">
</head>
<body>
<div class="page-main">
<h2>글쓰기</h2>
<form action="insert.do" method="post" th:object="${boardVO}">
<ul>
<li>
<label th:for="*{writer}">작성자</label>
<input type="text" th:field="*{writer}" th:errorclass="field-error">
<span th:errors="*{writer}" class="error-color"></span>
</li>
<li>
<label th:for="*{title}">제목</label>
<input type="text" th:field="*{title}" th:errorclass="field-error">
<span th:errors="*{title}" class="error-color"></span>
</li>
<li>
<label th:for="*{passwd}">비밀번호</label>
<input type="password" th:field="*{passwd}" th:errorclass="field-error">
<span th:errors="*{passwd}" class="error-color"></span>
</li>
<li>
<label th:for="*{content}">내용</label>
<textarea th:field="*{content}" th:errorclass="field-error"></textarea>
<span th:errors="*{content}" class="error-color"></span>
</li>
</ul>
<div class="align-center">
<input type="submit" value="등록">
<input type="button" value="목록" onclick="location.href='list.do'">
</div>
</form>
</div>
</body>
</html>
글 번호 : [[${board.num}]]
작성자 : [[${board.writer}]]
작성일 : [[${board.reg_date}]]
게시글의 번호, 작성자, 작성일을 출력한다.
[[...]]는 Thymeleaf의 텍스트 선언 문법으로, 데이터 바인딩을 의미한다.
${board.num}, ${board.writer}, ${board.reg_date}는 각각 게시글 객체의 필드를 참조한다.
th:text="${board.content}" 는 게시글 객체의 content 필드를 출력한다.
<p>
<span th:text="'글번호 : ' + ${board.num}"></span>
</p>
이 코드도 문제 없이 작성이 되기는 하지만 이렇게 작성하게 되면 더 복잡하게 느껴지기 때문에 [[...]]형태를 사용해준다.
<input type="button" value="수정" th:onclick="|location.href='@{update.do(num=${board.num})}'|" />
<input type="button" value="삭제" th:onclick="|location.href='@{delete.do(num=${board.num})}'|" />
th:onclick="|location.href='@{update.do(num=${board.num})}'|"
th:onclick="|location.href='@{delete.do(num=${board.num})}'|"
이 코드는 Thymeleaf의 텍스트 리터럴(|...|) 문법을 사용하여 버튼이 클릭되었을 때 JavaScript의 location.href를 설정
@{update.do(num=${board.num})}
@{delete.do(num=${board.num})}
Thymeleaf의 URL 표현식으로,
/update.do라는 URL에 num 파라미터를 포함하여 생성
/delete.do라는 URL에 num 파라미터를 포함하여 생성
${board.num}은 Thymeleaf 표현식으로 게시글 번호를 가져오는 부분
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>글 세부 정보</title>
<link rel="stylesheet" th:href="@{/css/style.css}" type="text/css">
</head>
<body>
<div class="page-main">
<h2 th:text="${board.title}"></h2>
<p>
글 번호 : [[${board.num}]] <br>
작성자 : [[${board.writer}]] <br>
작성일 : [[${board.reg_date}]] <br>
</p>
<hr width="100%" size="1" noshade="noshade">
<p th:text="${board.content}"></p>
<div class="align-center">
<input type="button" value="수정" th:onclick="|location.href='@{update.do(num=${board.num})}'|" />
<input type="button" value="삭제" th:onclick="|location.href='@{delete.do(num=${board.num})}'|" />
<input type="button" value="목록" onclick="location.href='/list.do'">
</div>
</div>
</body>
</html>
th:object="${boardVO}"
폼의 데이터를 바인딩할 객체를 지정한다. Thymeleaf의 표현식으로, 컨트롤러에서 전달받은 게시글 객체를 의미
th:field="*{num}"
<input type="hidden" th:field="*{num}">
숨은 입력 필드로, num 필드를 표시한다. 이 필드는 수정할 게시글의 번호를 서버에 전달하기 위해 사용
입력받는 내용들의 코드는 insertForm과 동일하기 때문에 추가적인 설명은 생략
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>게시글 수정하기</title>
<link rel="stylesheet" th:href="@{/css/style.css}" type="text/css">
</head>
<body>
<div class="page-main">
<h2>글쓰기</h2>
<form action="update.do" method="post" th:object="${boardVO}">
<input type="hidden" th:field="*{num}">
<ul>
<li>
<label th:for="*{writer}">작성자</label>
<input type="text" th:field="*{writer}" th:errorclass="field-error">
<span th:errors="*{writer}" class="error-color"></span>
</li>
<li>
<label th:for="*{title}">제목</label>
<input type="text" th:field="*{title}" th:errorclass="field-error">
<span th:errors="*{title}" class="error-color"></span>
</li>
<li>
<label th:for="*{passwd}">비밀번호</label>
<input type="password" th:field="*{passwd}" th:errorclass="field-error">
<span th:errors="*{passwd}" class="error-color"></span>
</li>
<li>
<label th:for="*{content}">내용</label>
<textarea th:field="*{content}" th:errorclass="field-error"></textarea>
<span th:errors="*{content}" class="error-color"></span>
</li>
</ul>
<div class="align-center">
<input type="submit" value="수정">
<input type="button" value="목록" onclick="location.href='list.do'">
</div>
</form>
</div>
</body>
</html>
th:field="*{num}"
<input type="hidden" th:field="*{num}">
숨은 입력 필드로, num 필드를 표시한다. 이 필드는 삭제할 게시글의 번호를 서버에 전달하기 위해 사용
비밀번호를 입력하는 것도 insertForm과 동일하기 때문에 추가적인 설명은 생략
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>게시글 삭제하기</title>
<link rel="stylesheet" th:href="@{/css/style.css}" type="text/css">
</head>
<body>
<div class="page-main">
<h2>글 삭제</h2>
<form action="delete.do" method="post" th:object="${boardVO}">
<input type="hidden" th:field="*{num}">
<ul>
<li>
<label th:for="*{passwd}">비밀번호</label>
<input type="password" th:field="*{passwd}" th:errorclass="field-error">
<span th:errors="*{passwd}" class="error-color"></span>
</li>
</ul>
<div class="align-center">
<input type="submit" value="삭제">
<input type="button" value="목록" onclick="location.href='list.do'">
</div>
</form>
</div>
</body>
</html>
게시판 목록

글 작성하기 유효성 체크

글 작성하기

글 작성 완료 후 글 세부 보기

글 수정하기 유효성 체크

글 수정하기 비밀번호 일치 여부 체크

글 수정하기

글 수정 후 글 세부 보기

글 삭제 비밀번호 유효성 체크

글 삭제 비밀번호 일치 여부 체크

글 삭제 후 글 목록
