웹 MVC에서 스프링 프레임워크와 스프링 MVC으로의 이전
XML 설정 -> 어노테이션을 이용한 자바 설정
사용 기술
스프링 프레임워크
MyBatis
스프링 MVC
요구사항
CRUD
페이징
검색
- 검색과 필터링 가능한 화면
- MyBatis의 동적 쿼리을 이용해 해당하는 Todo검색
- 새로운 Todo 등록시 문자열, boolean, LocalDate를 자동으로 처리
- 목록에서 조회 화면 이동시 모든 검색, 필터링, 페이징 조건 유지
- 조회 화면에서 모든 조건 유지한 채로, 수정/삭제 화면으로 이동
- 삭제 후, 다시 목록 화면으로 이동 (PRG)
- 수정 후, 다시 조회 화면으로 이동하지만 검색, 필터링, 페이지 조건은 초기화 (PRG)
한글 깨짐 처리를 위한 인코딩 설정
1. VM 옵션
2. File Encoding 옵션
3. gradle -intellij 옵션
라이브러리 의존성 추가
1. 스프링 관련
implementation group: 'org.springframework', name: 'spring-core', version: '5.3.19'
implementation group: 'org.springframework', name: 'spring-context', version: '5.3.19'
implementation group: 'org.springframework', name: 'spring-test', version: '5.3.19'
implementation group: 'org.springframework', name: 'spring-webmvc', version: '5.3.19'
implementation group: 'org.springframework', name: 'spring-jdbc', version: '5.3.19'
implementation group: 'org.springframework', name: 'spring-tx', version: '5.3.19'
2. MyBatis/Maria DB / HikariCP
//MyBatis/MariaDB HikariCP
implementation 'org.mariadb.jdbc:mariadb-java-client:3.0.3'
implementation group: 'com.zaxxer', name: 'HikariCP', version: '5.0.1'
implementation 'org.mybatis:mybatis:3.5.9'
implementation 'org.mybatis:mybatis-spring:2.0.7'
3. JSTL
//JSTL
implementation group: 'jstl', name: 'jstl', version: '1.2'
4. ModelMapper
//ModelMapper
implementation group: 'org.modelmapper', name: 'modelmapper', version: '3.0.0'
5. Validate
-DTO 검증하는 라이브러리
//Validate
implementation group: 'org.hibernate', name: 'hibernate-validator', version: '6.2.1.Final'
6. Lombok
//Lombok
compileOnly 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
testCompileOnly 'org.projectlombok:lombok:1.18.24'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.24'
7. log4j2
//log4j
implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.17.2'
implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.17.2'
implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.17.2'
-> 스프링 프레임워크에서는 버전을 동일하게 맞춰줘야 한다.
1. HelloServlet 삭제
2. webapp에서 todo 폴더만 이용
3. sample 관련된 파일과 설정은 삭제
1. table 생성
drop table tbl_todo;
create table tbl_todo(
tno int auto_increment primary key ,
title varchar(100) not null,
dueDate date not null ,
writer varchar(50) not null ,
finished tinyint default 0
);
2. ModelMapper 설정과 @Configuration
(1) DTO <-> VO를 위한 ModelMapper 생성
-config 패키지에 ModelMapperConfig 클래스 작성
-ModelMapperConfig는 @Configuration을 추가해 스프링으로 변경한 MapperUtil 클래스
-@Configuration : 해당 클래스가 스프링 빈에 대한 설정을 하는 클래스로 사용하기 위한 어노테이션
변경 전, MapperUtil
-util패키지를 추가해 ModelMapper 설정 변경하기 위한 MapperUtil을 enum으로 생성
-getConfiguraion()을 이용해 ModelMapper 설정 변경
-get()을 이용해 ModelMapper 사용
package org.zerock.jdbcex.util;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
public enum MapperUtil {
INSTANCE;
private ModelMapper modelMapper;
MapperUtil() {
this.modelMapper = new ModelMapper();
this.modelMapper.getConfiguration()
.setFieldMatchingEnabled(true)
.setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
.setMatchingStrategy(MatchingStrategies.LOOSE);
}
public ModelMapper get() {
return modelMapper;
}
변경 후, MapperUtil
package org.zerock.springex.config;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ModelMapperConfig {
@Bean
public ModelMapper getMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration()
.setFieldMatchingEnabled(true)
.setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
.setMatchingStrategy(MatchingStrategies.LOOSE);
return modelMapper;
}
}
-@Bean 어노테이션 : 메소드의 반환값을 스프링의 빈으로 등록시키는 어노테이션
-getMapper() 메서드 : ModelMapper를 반환
(2) 빈으로 인식할 수 있도록, root-context.xml에 패키지 추가
<context:component-scan base-package="org.zerock.springex.config"/>
3. 부트스트랩 적용
-프로젝트 시작단계에서 화면 디자인을 결정해야 반복된 작업을 피할 수 있다.
(1) 부트스트랩 적용 페이지 webapp/resources/test.html 작성
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- 기존의 <h1>Header</h1> -->
<div class="row">
<div class="col">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link active" aria-current="page" href="#">Home</a>
<a class="nav-link" href="#">Features</a>
<a class="nav-link" href="#">Pricing</a>
<a class="nav-link disabled">Disabled</a>
</div>
</div>
</div>
</nav>
</div>
</div>
<!-- header end -->
<!-- 기존의 <h1>Header</h1>끝 -->
<div class="row content">
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<h5 class="card-title">Special title treatment</h5>
<p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
</div>
</div>
</div>
<div class="row content">
<h1>Content</h1>
</div>
<div class="row footer">
<!--<h1>Footer</h1>-->
<div class="row fixed-bottom" style="z-index: -100">
<footer class="py-1 my-1 ">
<p class="text-center text-muted">Footer</p>
</footer>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</body>
</html>
-> 부트스트랩 템플릿 적용하기 위한 css와 js import
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
(2) 부트스트랩 container, row 적용
-레이아웃 구성하기 위해 화면 구성에서 사용하는 구성요소
<div class="container-fluid">
<div class="row">
<h1>Header</h1>
</div>
<div class="row content">
<h1>Content</h1>
</div>
<div class="row footer">
<h1>Footer</h1>
</div>
</div>
(3) Card 컴포넌트 적용
컴포넌트 : 화면을 쉽게 제작하기 위해 부트스트랩에서 제공하는 요소
https://getbootstrap.kr/docs/5.1/components/card/
헤더와 푸터 기본 구조
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<h5 class="card-title">Special title treatment</h5>
<p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
-> 행 본문에 열로 추가
<div class="container-fluid">
<div class="row">
<h1>Header</h1>
<div class="row content">
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<h5 class="card-title">Special title treatment</h5>
<p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
</div>
</div>
</div>
<div class="row content">
<h1>Content</h1>
</div>
<div class="row footer">
<h1>Footer</h1>
</div>
</div>
(4) Navbar 컴포넌트 적용
-네비게이션 바 컴포넌트 추가
https://getbootstrap.kr/docs/5.1/components/navbar/
검색창이 없는 기본 네비게이션 구조
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Features</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Pricing</a>
</li>
<li class="nav-item">
<a class="nav-link disabled">Disabled</a>
</li>
</ul>
</div>
</div>
</nav>
-> navbar 컴포넌트는 브라우저 크기에 따라서 메뉴들이 보이거나 사라지는 반응형으로 동작한다.
<h1> 태그 부분에 네비게이션 바를 추가한다.
<div class="container-fluid">
<div class="row">
<!-- 기존의 <h1>Header</h1> -->
<div class="row">
<div class="col">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link active" aria-current="page" href="#">Home</a>
<a class="nav-link" href="#">Features</a>
<a class="nav-link" href="#">Pricing</a>
<a class="nav-link disabled">Disabled</a>
</div>
</div>
</div>
</nav>
</div>
</div>
<!-- header end -->
<!-- 기존의 <h1>Header</h1>끝 -->
<div class="row content">
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<h5 class="card-title">Special title treatment</h5>
<p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
</div>
</div>
</div>
<div class="row content">
<h1>Content</h1>
</div>
<div class="row footer">
<h1>Footer</h1>
</div>
</div>
footer 적용
footer 기본 구조
<div class="card">
<div class="card-header">
Quote
</div>
<div class="card-body">
<blockquote class="blockquote mb-0">
<p>A well-known quote, contained in a blockquote element.</p>
<footer class="blockquote-footer">Someone famous in <cite title="Source Title">Source Title</cite></footer>
</blockquote>
</div>
</div>
-footer는 항상 맨 하단에 위치한 요소로, fixed-bottom을 적용해 맨 아래쪽에 적용할 수 있다.
-footer 영역으로 인해 가려지는 경우를 방지하기 위해 z-index값을 음수로 처리한다.
<div class="container-fluid">
<div class="row">
<!-- 기존의 <h1>Header</h1> -->
<div class="row">
<div class="col">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link active" aria-current="page" href="#">Home</a>
<a class="nav-link" href="#">Features</a>
<a class="nav-link" href="#">Pricing</a>
<a class="nav-link disabled">Disabled</a>
</div>
</div>
</div>
</nav>
</div>
</div>
<!-- header end -->
<!-- 기존의 <h1>Header</h1>끝 -->
<div class="row content">
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<h5 class="card-title">Special title treatment</h5>
<p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
</div>
</div>
</div>
<div class="row content">
<h1>Content</h1>
</div>
<div class="row footer">
<!--<h1>Footer</h1>-->
<div class="row fixed-bottom" style="z-index: -100">
<footer class="py-1 my-1 ">
<p class="text-center text-muted">Footer</p>
</footer>
</div>
</div>
</div>
4. MyBatis와 스프링을 이용한 영속 처리
-MyBatis와 스프링을 연동해 데이터베이스 처리
MyBatis 개발 단계
- VO 선언
- Mapper 인터페이스 개발
- XML 개발
- 테스트 코드 개발
1. VO 선언
-domain 패키지 추가해 TodoVO 인터페이스 작성
package org.zerock.springex.domain;
import lombok.*;
import java.time.LocalDate;
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TodoVO {
private Long tno;
private String title;
private LocalDate dueDate;
private String writer;
private boolean finished;
}
2. Mapper 인터페이스 개발
-TodoMapper 인터페이스 작성
-TodoVO가 매퍼 인터페이스의 파라미터나 리턴 타입이 될 수 있기 때문에
package org.zerock.springex.domain;
public interface TodoMapper {
String getTime();
}
3. XML 개발
주의사항
- (namespace 값) = ( 인터페이스 이름)
- (메소드 이름) = (<select>태그의 id 값)
-TodoMapper.xml 작성
package org.zerock.springex.mapper;
import org.zerock.springex.domain.TodoVO;
import org.zerock.springex.dto.PageRequestDTO;
import java.util.List;
public interface TodoMapper {
String getTime();
void insert(TodoVO todoVO);
List<TodoVO> selectAll();
TodoVO selectOne(Long tno);
void delete(Long tno);
void update(TodoVO todoVO);
List<TodoVO> selectList(PageRequestDTO pageRequestDTO);
int getCount(PageRequestDTO pageRequestDTO);
}
4. 테스트 코드 개발
(1) TodoMapperTests 클래스 작성
package org.zerock.springex.mapper;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.zerock.springex.domain.TodoVO;
import org.zerock.springex.dto.PageRequestDTO;
import java.time.LocalDate;
import java.util.List;
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations="file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoMapperTests {
@Autowired(required = false)
private TodoMapper todoMapper;
@Test
public void testGetTime() {
log.info(todoMapper.getTime());
}
}
(2) 로그 수준을 높이기 위한 resources/log4j2.xml 설정
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="INFO">
<Appenders>
<!-- 콘솔 -->
<Console name="console" target="SYSTEM_OUT">
<PatternLayout charset="UTF-8" pattern="%d{hh:mm:ss} %5p [%c] %m%n"/>
</Console>
</Appenders>
<loggers>
<logger name="org.springframework" level="INFO" additivity="false">
<appender-ref ref="console" />
</logger>
<logger name="org.zerock" level="INFO" additivity="false">
<appender-ref ref="console" />
</logger>
<logger name="org.zerock.springex.mapper" level="TRACE" additivity="false">
<appender-ref ref="console" />
</logger>
<root level="INFO" additivity="false">
<AppenderRef ref="console"/>
</root>
</loggers>
</configuration>
5. Todo 기능 개발
- 등록
- 목록
- 조회
- 수정/삭제
- 검색
- 페이징/필터링
등록
- TodoMapper : 객체와 SQL을 매핑하는 클래스 -> 요청한 쿼리를 수행하는 XML
- TodoService : register()
- TodoController : urlpatterns -> @GetMapping, @PostMapping -> register()
- JSP : <form> -> submit
(1) TodoMapper
TodoMapper.java
public interface TodoMapper {
String getTime();
void insert(TodoVO todoVO);
}
resources/mappers/TodoMapper.xml
-문서 타입과, 네임스페이스 설정
-클래스의 메서드이름을 id값으로 사용해 SQL 구현 (select문 경우 resultType 필요)
<?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="org.zerock.springex.mapper.TodoMapper">
</mapper>
<mapper namespace="org.zerock.springex.mapper.TimeMapper2">
<select id="getTime" resultType="">
select now()
</select>
<insert id="insert">
insert into tbl_todo (title, dueDate, writer) (#{title}, #{dueDate}, {writer})
</insert>
</mapper>
-MyBatis에서는 JDBC에서 사용하던 '?'대신에 EL 표현식과 비슷한 '#{title}'과 같이 파라미터를 처리한다.
root-context.xml에 mapper 경로 지정
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:/mappers/**/*.xml"></property>
</bean>
<mybatis:scan base-package="org.zerock.springex.mapper"></mybatis:scan>
#unmappable character for encoding UTF-8 발생할 경우
- VM 설정 확인
- preferences - editor - file encodings -utf-8 확인
- gradle -intellij 확인
MapperTests 테스트 코드 작성
package org.zerock.mapper;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.zerock.springex.domain.TodoVO;
import org.zerock.springex.mapper.TodoMapper;
import java.time.LocalDate;
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoMapperTests {
@Autowired(required = false)
private TodoMapper todoMapper;
@Test
public void testInsert(){
TodoVO todoVO =TodoVO.builder()
.title("스프링 테스트")
.dueDate(LocalDate.of(2022,10,10))
.writer("user00")
.build();
todoMapper.insert(todoVO);
}
}
(2) TodoService 인터페이스와 인터페이스를 구현한 TodoServiceImpl 클래스
-TodoMapper와 TodoController 사이에서 전달하는 서비스 계층 클래스 TodoService
-TodoService 인터페이스를 추가 -> 인터페이스를 구현한 클래스를 스프링 빈으로 등록하는 역할을 수행
-TodoMapper : 객체 -> SQL
-ModelMapper : DTO <-> VO
package org.zerock.springex.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import org.zerock.springex.domain.TodoVO;
import org.zerock.springex.dto.TodoDTO;
import org.zerock.springex.mapper.TodoMapper;
@Service
@Log4j2
@RequiredArgsConstructor
public class TodoServiceImpl implements TodoService{
private final TodoMapper todoMapper;
private final ModelMapper modelMapper;
@Override
public void register(TodoDTO todoDTO) {
log.info("register");
log.info(modelMapper);
TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);
log.info(todoVO);
todoMapper.insert(todoVO);
}
}
-@RequiredArgsConstructor : final 선언한 객체의 생성자를 생성해 의존성을 주입하는 어노테이션
-register() 메서드 : ModelMapper로 DTO를 VO로 변환 -> Mapper클래스의 파리미터로 VO 전달해 insert 메서드 호출
service 패키지를 root-context.xml에서 컴포넌트 스캔 대상으로 추가
<mybatis:scan base-package="org.zerock.springex.mapper"></mybatis:scan>
<context:component-scan base-package="org.zerock.springex.config"/>
<context:component-scan base-package="org.zerock.springex.service"/>
테스트 코드 작성
package org.zerock.service;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.zerock.springex.dto.TodoDTO;
import org.zerock.springex.service.TodoService;
import java.time.LocalDate;
@Log4j2
@ExtendWith(SpringExtension.class)
//어떤 컨텍스트에서 테스트를 수행할지 결정하는 xml 설정파일 경로 지정 어노테이션
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoServiceTests {
@Autowired
private TodoService todoService;
@Test
public void testRegister(){
TodoDTO todoDTO = TodoDTO.builder()
.title("Test...")
.dueDate(LocalDate.now())
.writer("user1")
.build();
todoService.register(todoDTO);
}
}
(3) TodoController를 통해 URL 매핑과 GET/POST 처리
-데이터 계층 -> 서비스 계층 -> 웹 계층 [스프링 MVC]
입력 화면을 처리하기 위해 컨트롤러 변경
@Controller
@RequestMapping("/todo")
@Log4j2
@RequiredArgsConstructor
public class TodoController {
private final TodoService todoService;
// @RequestMapping("/list")
// public void list(Model model){
//
// log.info("todo list.......");
//
// //model.addAttribute("dtoList", todoService.getAll());
// }
@GetMapping("/register")
public void registerGET() {
log.info("GET todo register.......");
}
}
/WEB-INF/views/todo/register.jsp 생성
-test.html을 템플릿으로 활용해 작성
-JSP 관련 설정과 JSTL 관련 설정 추가
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-card 컴포넌트의 body 부분에 추가
<div class="card-body">
<form action="/todo/register" method="post">
<div class="input-group mb-3">
<span class="input-group-text">Title</span>
<input type="text" name="title" class="form-control" placeholder="Title">
</div>
<div class="input-group mb-3">
<span class="input-group-text">DueDate</span>
<input type="date" name="dueDate" class="form-control" placeholder="Writer">
</div>
<div class="input-group mb-3">
<span class="input-group-text">Writer</span>
<input type="text" name="writer" class="form-control" placeholder="Writer">
</div>
<div class="my-4">
<div class="float-end">
<button type="submit" class="btn btn-primary">Submit</button>
<button type="result" class="btn btn-secondary">Reset</button>
</div>
</div>
</form>
#주소를 못 찾는 경우 root-context.xml, servlet-context.xml, Web.xml 설정 확인
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/servlet-context.xml</param-value>
</init-param>
<init-param>
<param-name>throwExceptionIfNoHandlerFound</param-name>
<param-value>true</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<filter>
<filter-name>encoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encoding</filter-name>
<servlet-name>appServlet</servlet-name>
</filter-mapping>
</web-app>
-> Content 주석처리
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- 기존의 <h1>Header</h1> -->
<div class="row">
<div class="col">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link active" aria-current="page" href="#">Home</a>
<a class="nav-link" href="#">Features</a>
<a class="nav-link" href="#">Pricing</a>
<a class="nav-link disabled">Disabled</a>
</div>
</div>
</div>
</nav>
</div>
</div>
<!-- header end -->
<!-- 기존의 <h1>Header</h1>끝 -->
<div class="row content">
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<form action="/todo/register" method="post">
<div class="input-group mb-3">
<span class="input-group-text">Title</span>
<input type="text" name="title" class="form-control" placeholder="Title">
</div>
<div class="input-group mb-3">
<span class="input-group-text">DueDate</span>
<input type="date" name="dueDate" class="form-control" placeholder="Writer">
</div>
<div class="input-group mb-3">
<span class="input-group-text">Writer</span>
<input type="text" name="writer" class="form-control" placeholder="Writer">
</div>
<div class="my-4">
<div class="float-end">
<button type="submit" class="btn btn-primary">Submit</button>
<button type="result" class="btn btn-secondary">Reset</button>
</div>
</div>
</form>
<script>
const serverValidResult = {}
<c:forEach items="${errors}" var="error">
serverValidResult['${error.getField()}'] = '${error.defaultMessage}'
</c:forEach>
console.log(serverValidResult)
</script>
</div>
</div>
</div>
</div>
</div>
<div class="row content">
<!-- <h1>Content</h1>-->
</div>
<div class="row footer">
<!--<h1>Footer</h1>-->
<div class="row fixed-bottom" style="z-index: -100">
<footer class="py-1 my-1 ">
<p class="text-center text-muted">Footer</p>
</footer>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</body>
</html>
(4) POST 방식 처리 (입력 폼 전송)
- <form action="/todo/register" method="post"> 태그로 POST 방식으로 데이터 전송
- TodoController에서 TodoDTO로 입력받은 파라미터 값들을 수집해, 가공 후 처리한다.
- PRG패턴을 적용해, 입력 처리 후에는 목록화면으로 이동
@PostMapping("/register")
public String registerPost(@Valid TodoDTO todoDTO,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
log.info("POST todo register.......");
if(bindingResult.hasErrors()) {
log.info("has errors.......");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors() );
return "redirect:/todo/register";
}
log.info(todoDTO);
todoService.register(todoDTO);
return "redirect:/todo/list";
}
개선해야할 사항
- 한글 깨짐
- 유효하지 않는 데이터를 전달할 경우
브라우저에서의 한글 처리를 위한 필터 설정
1. 브라우저에서 서버로 데이터 전송시 한글 깨지는 경우
2. 서버에서 한글이 깨지는 경우
-스프링 MVC가 제공하는 필터로 서버의 한글 처리 수행
(1) web.xml에 인코딩 필터 설정 추가
<filter>
<filter-name>encoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encoding</filter-name>
<servlet-name>appServlet</servlet-name>
</filter-mapping>
@Valid과 BindingResult 이용한 서버 사이드 검증
데이터 검증 이중 검증
- 브라우저의 프론트단 검증
- 서버 사이드에서 검증
의존성 추가
//Validate
implementation group: 'org.hibernate', name: 'hibernate-validator', version: '6.2.1.Final'
hibernate-validator 어노테이션
- @NotNull, @Null, @NotEmpty, @NotBlank
- @Size(min=, max=), @Pattern(regex=)
- @Max(num), Min(num)
- @Future @Past
- @Positive @PositiveOrZero @Negative @NegativeOrZero
(1) TodoDTO 검증
-필수 요소 : @NotEmpty
-해야 할일 이므로, 미래만 사용 : @Future
package org.zerock.springex.dto;
import lombok.*;
import javax.validation.constraints.Future;
import javax.validation.constraints.NotEmpty;
import java.time.LocalDate;
@ToString
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TodoDTO {
private Long tno;
@NotEmpty
private String title;
@Future
private LocalDate dueDate;
private boolean finished;
@NotEmpty
private String writer;
}
(2) TodoDTO를 POST 방식 처리시, 검증 사항을 반영하기 위해 컨트롤러에 BindingResult와 @Valid 적용
변경 전, registerPost()
@PostMapping("/register")
public String registerPost(@Valid TodoDTO todoDTO,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
log.info("POST todo register.......");
if(bindingResult.hasErrors()) {
log.info("has errors.......");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors() );
return "redirect:/todo/register";
}
log.info(todoDTO);
todoService.register(todoDTO);
return "redirect:/todo/list";
}
변경 후, registerPost()
-TodoDTO에 @Valid 적용
-BindingResult 타입을 파라미터로 추가
-hasErrors() 메서드 : 예외처리 메서드로, 에러가 발견되면 입력 화면으로 리다이렉트하며 addFlashAttribute()로 해당 데이터 전송
@PostMapping("/register")
public String registerPost(@Valid TodoDTO todoDTO,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
log.info("POST todo register.......");
if(bindingResult.hasErrors()) {
log.info("has errors.......");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors() );
return "redirect:/todo/register";
}
log.info(todoDTO);
//todoService.register(todoDTO);
return "redirect:/todo/list";
}
(3) JSP에서 검증 에러 메시지 확인
-자바스크립트 객체를 이용해 에러 메시지 화면 처리
<script>
const serverValidResult = {}
<c:forEach items="${errors}" var="error">
serverValidResult['${error.getField()}'] = '${error.defaultMessage}'
</c:forEach>
console.log(serverValidResult)
</script>
-> 문자열로 처리하는 이유는 나중에 RESTful을 적용했을 때, JSON과 함께 자바스크립트에서 처리할 수 있도록 하기 위해서
(4) TodoService 주입과 연동
-TodoController에서 TodoService 주입 후, registerPost()에서 todoService 클래스의 register() 메서드 호출해 todoDTO를 파라미터로 전달
@Controller
@RequestMapping("/todo")
@Log4j2
@RequiredArgsConstructor
public class TodoController {
private final TodoService todoService;
@RequestMapping("/list")
public void list(Model model){
log.info("todo list.......");
//model.addAttribute("dtoList", todoService.getAll());
}
@GetMapping("/register")
public void registerGET() {
log.info("GET todo register.......");
}
@PostMapping("/register")
public String registerPost(@Valid TodoDTO todoDTO,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
log.info("POST todo register.......");
if(bindingResult.hasErrors()) {
log.info("has errors.......");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors() );
return "redirect:/todo/register";
}
log.info(todoDTO);
todoService.register(todoDTO);
return "redirect:/todo/list";
}
}
목록
-목록 데이터 출력 기능 부터 우선개발 후, 페이징 처리와 검색 기능 추가
- TodoMapper : 객체와 SQL을 매핑하는 클래스 -> 요청한 쿼리를 수행하는 XML
- TodoService : register()
- TodoController : urlpatterns -> @GetMapping, @PostMapping -> register()
- JSP : <form> -> submit
1. TodoMapper 개발
(1) TodoMapper 인터페이스에 selectAll() 메서드 추가
List<TodoVO> selectAll();
(2) TodoMapper.xml에 내림차순 selectAll() 쿼리문 작성
- <select>의 경우 resultType 속성이 반드시 필요하다.
- resultType : JDBC의 ResultSet의 한 행의 타입을 결정
<select id="selectAll" resultType="org.zerock.springex.domain.TodoVO">
select * from tbl_todo order by tno desc
</select>
(3) Mapper 테스트 코드 작성
@Test
public void testSelectAll(){
List<TodoVO> voList=todoMapper.selectAll();
voList.forEach(vo -> log.info(vo));
}
2. TodoService/TodoServiceImpl 개발
-TodoMapper의 getAll() 메서드 반환 타입 : List<TodoVO>
-필요한 타입 : List<TodoDTO>
(1) TodoService 인터페이스에 getAll() 메서드 추가
(2) TodoServiceImpl에 getAll() 메서드 오버라이딩
-람다와 스트림을 이용해 List<TodoVO>을 List<TodoDTO>로 변환
스트림의 map() 메서드를 이용한 변환
package lamdaAndstream;
import java.io.File;
import java.util.stream.Stream;
public class Stream2 {
public static void main(String[] args) {
File[] fileArr = { new File("Ex1.java"), new File("Ex1.bak"),
new File("Ex2.java"), new File("Ex1"), new File("Ex1.txt")
};
Stream<File> fileStream = Stream.of(fileArr);
// map()으로 Stream<File>을 Stream<String>으로 변환
Stream<String> filenameStream = fileStream.map(File::getName);
filenameStream.forEach(System.out::println); // 모든 파일의 이름을 출력
fileStream = Stream.of(fileArr); // 스트림을 다시 생성
//스트림을 이용한 변환
// 1. Stream<File> → Stream<String>
// 2. 확장자가 없는 것은 제외
// 3. 확장자만 추출
// 4. 모두 대문자로 변환
// 5. 중복 제거
// 6. JAVABAKTXT
System.out.println();
}
}
fileStream.map(File::getName) // Stream<File> → Stream<String>
.filter(s -> s.indexOf('.')!=-1) // 확장자가 없는 것은 제외
.map(s -> s.substring(s.indexOf('.')+1)) // 확장자만 추출
.map(String::toUpperCase) // 모두 대문자로 변환
.distinct() // 중복 제거
.forEach(System.out::print); // JAVABAKTXT
map()과 modelMapper를 이용해 변환
-map() : 스트림에서 람다식을 이용해 변환
-modelMapper.map() : DTO <-> VO 변환을 담당하는 라이브러리에서 람다식을 이용해 변환
@Override
public List<TodoDTO> getAll() {
List<TodoDTO> dtoList = todoMapper.selectAll().stream()
.map(vo -> modelMapper.map(vo, TodoDTO.class))
.collect(Collectors.toList());
return dtoList;
}
3. TodoController 개발
-list() 메서드 : TodoService의 getAll() 메서드 호출해, Model에 담아 JSP에 전달
@RequestMapping("/list")
public void list(Model model){
log.info("todo list.......");
model.addAttribute("dtoList", todoService.getAll());
}
4. JSP 개발
-전달 받은 dtoList를 JSTL을 이용해 목록 출력
- <c:forEach> : 목록 출력 메서드
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- 기존의 <h1>Header</h1> -->
<div class="row">
<div class="col">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link active" aria-current="page" href="#">Home</a>
<a class="nav-link" href="#">Features</a>
<a class="nav-link" href="#">Pricing</a>
<a class="nav-link disabled">Disabled</a>
</div>
</div>
</div>
</nav>
</div>
</div>
<!-- header end -->
<!-- 기존의 <h1>Header</h1>끝 -->
<div class="row content">
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<h5 class="card-title">Special title treatment</h5>
<table class="table">
<thead>
<tr>
<th scope="col">Tno</th>
<th scope="col">Title</th>
<th scope="col">Writer</th>
<th scope="col">DueDate</th>
<th scope="col">Finished</th>
</tr>
</thead>
<tbody>
<c:forEach items="${dtoList}" var="dto">
<tr>
<th scope="row"><c:out value="${dto.tno}"/></th>
<td><c:out value="${dto.title}"/></td>
<td><c:out value="${dto.writer}"/></td>
<td><c:out value="${dto.dueDate}"/></td>
<td><c:out value="${dto.finished}"/></td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row content">
</div>
<div class="row footer">
<!--<h1>Footer</h1>-->
<div class="row fixed-bottom" style="z-index: -100">
<footer class="py-1 my-1 ">
<p class="text-center text-muted">Footer</p>
</footer>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</body>
</html>
조회
'/todo/read?tno=xx'와 같이 쿼리스트링을 이용한 주소를 TodoController를 통해 호출
- TodoMapper : 객체와 SQL을 매핑하는 클래스 -> 요청한 쿼리를 수행하는 XML
- TodoService : register()
- TodoController : urlpatterns -> @GetMapping, @PostMapping -> register()
- JSP : <form> -> submit
1. TodoMapper 개발
-selectOne() 메서드 작성
<select id="selectOne" resultType="org.zerock.springex.domain.TodoVO">
select * from tbl_todo where tno=#{tno}
</select>
테스트 코드 작성
@Test
public void testSelectOne(){
TodoVO vo=todoMapper.selectOne(3L);
log.info(vo);
}
2. TodoService 개발
-getOne() 메서드 작성 후, TodoMapper의 selectOne() 메서드 호출해 ModelMapper로 파라미터 전달해 VO를 DTO로 반환
@Override
public TodoDTO getOne(Long tno) {
TodoVO vo=todoMapper.selectOne(tno);
TodoDTO dto = modelMapper.map(vo, TodoDTO.class);
return dto;
}
3. TodoController 개발
-GET방식의 read() 메서드 작성해, TodoService의 getOne() 메서드 호출
@GetMapping("/read")
public void read(Long tno, Model model){
TodoDTO todoDTO = todoService.getOne(tno);
log.info(todoDTO);
model.addAttribute("dto", todoDTO);
}
4. JSP 개발
-JSTL을 이용해 dto라는 이름으로 전달된 TodoDTO 출력하는 read.jsp 작성
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- 기존의 <h1>Header</h1> -->
<div class="row">
<div class="col">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link active" aria-current="page" href="#">Home</a>
<a class="nav-link" href="#">Features</a>
<a class="nav-link" href="#">Pricing</a>
<a class="nav-link disabled">Disabled</a>
</div>
</div>
</div>
</nav>
</div>
</div>
<!-- header end -->
<!-- 기존의 <h1>Header</h1>끝 -->
<div class="row content">
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<div class="input-group mb-3">
<span class="input-group-text">TNO</span>
<input type="text" name="tno" class="form-control"
value=<c:out value="${dto.tno}"></c:out> readonly>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Title</span>
<input type="text" name="title" class="form-control"
value=<c:out value="${dto.title}"></c:out> readonly>
</div>
<div class="input-group mb-3">
<span class="input-group-text">DueDate</span>
<input type="date" name="dueDate" class="form-control"
value=<c:out value="${dto.dueDate}"></c:out> readonly>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Writer</span>
<input type="text" name="writer" class="form-control"
value=<c:out value="${dto.writer}"></c:out> readonly>
</div>
<div class="form-check">
<label class="form-check-label" >
Finished
</label>
<input class="form-check-input" type="checkbox" name="finished" ${dto.finished?"checked":""} disabled >
</div>
<div class="my-4">
<div class="float-end">
<button type="button" class="btn btn-primary">Modify</button>
<button type="button" class="btn btn-secondary">List</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row content">
</div>
<div class="row footer">
<!--<h1>Footer</h1>-->
<div class="row fixed-bottom" style="z-index: -100">
<footer class="py-1 my-1 ">
<p class="text-center text-muted">Footer</p>
</footer>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</body>
</html>
조회한 게시글에서 수정/삭제 처리를 위한 링크 처리
-Modify 버튼을 통해 수정/삭제 선택 가능한 화면으로 이동
<div class="my-4">
<div class="float-end">
<button type="button" class="btn btn-primary">Modify</button>
<button type="button" class="btn btn-secondary">List</button>
</div>
</div>
<script>
document.querySelector(".btn-primary").addEventListener("click", function(e){
self.location = "/todo/modify?tno="+${dto.tno}
},false)
</script>
목록 페이지에서 각각의 제목을 통해 링크 처리
- '/todo/read?tno=xxx'로 이동가능하도록 링크 처리
<tbody>
<c:forEach items="${dtoList}" var="dto">
<tr>
<th scope="row"><c:out value="${dto.tno}"/></th>
<td>
<a href="/todo/read?tno=${dto.tno}" class="text-decoration-none">
<c:out value="${dto.title}"/>
</a>
</td>
<td><c:out value="${dto.writer}"/></td>
<td><c:out value="${dto.dueDate}"/></td>
<td><c:out value="${dto.finished}"/></td>
</tr>
</c:forEach>
</tbody>
삭제
수정과 삭제는 GET 방식으로 조회한 후에 POST 방식으로 처리
GET방식은 조회와 같기 때문에 스프링 MVC에서 지원하는 경로를 배열처럼 표기하는 표기법을 이용해 하나의 @GetMapping으로 처리
-> 수정과 삭제에도 같은 메소드 이용하도록 read() 기능을 수정
1. TodoController의 read() 수정
@GetMapping({"/read", "/modify"})
public void read(Long tno, Model model){
TodoDTO todoDTO = todoService.getOne(tno);
log.info(todoDTO);
model.addAttribute("dto", todoDTO );
}
2. read.jsp를 수정해 modify.jsp 작성
-POST 방식으로, 제목/만료일/완료 수정 가능하도록 태그 구성 및 편집 가능하도록 변경
-제목/만료일/완료 외의 요소들은 'readonly' 속성으로 읽기만 가능하도록 설정
<div class="card-body">
<form action="/todo/modify" method="post">
<div class="input-group mb-3">
<span class="input-group-text">TNO</span>
<input type="text" name="tno" class="form-control"
value=<c:out value="${dto.tno}"></c:out> readonly>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Title</span>
<input type="text" name="title" class="form-control"
value=<c:out value="${dto.title}"></c:out> >
</div>
<div class="input-group mb-3">
<span class="input-group-text">DueDate</span>
<input type="date" name="dueDate" class="form-control"
value=<c:out value="${dto.dueDate}"></c:out> >
</div>
<div class="input-group mb-3">
<span class="input-group-text">Writer</span>
<input type="text" name="writer" class="form-control"
value=<c:out value="${dto.writer}"></c:out> readonly>
</div>
<div class="form-check">
<label class="form-check-label" >
Finished
</label>
<input class="form-check-input" type="checkbox" name="finished" ${dto.finished?"checked":""} >
</div>
</form>
버튼 클릭시 수정, 삭제하는 스크립트와 목록 화면으로 이동하는 스크립트 작성
<script>
const formObj = document.querySelector("form")
document.querySelector(".btn-danger").addEventListener("click",function(e) {
e.preventDefault()
e.stopPropagation()
formObj.action ="/todo/remove"
formObj.method ="post"
formObj.submit()
},false);
document.querySelector(".btn-primary").addEventListener("click",function(e) {
e.preventDefault()
e.stopPropagation()
formObj.action ="/todo/modify"
formObj.method ="post"
formObj.submit()
},false);
document.querySelector(".btn-secondary").addEventListener("click",function(e) {
e.preventDefault()
e.stopPropagation()
self.location = "/todo/list";
},false);
</script>
삭제/수정/목록 버튼 추가
<div class="my-4">
<div class="float-end">
<button type="button" class="btn btn-danger">Remove</button>
<button type="button" class="btn btn-primary">Modify</button>
<button type="button" class="btn btn-secondary">List</button>
</div>
</div>
3. Remove 버튼 처리
-자바스크립트를 이용해 <form> 태그의 action을 조정
(1) 클래스 속성이 btn-danger를 이용해 클릭 이벤트 처리
<script>
const formObj = document.querySelector("form")
document.querySelector(".btn-danger").addEventListener("click",function(e) {
e.preventDefault()
e.stopPropagation()
formObj.action ="/todo/remove"
formObj.method ="post"
formObj.submit()
},false);
</script>
(2) TodoControllder에서 POST방식으로 동작하는 remove() 메서드 설계
@PostMapping("/remove")
public String remove(Long tno, RedirectAttributes redirectAttributes){
log.info("-------------remove------------------");
log.info("tno: " + tno);
//todoService.remove(tno);
return "redirect:/todo/list";
}
(3) TodoServiceImpl 클래스에서 remove() 메서드 작성 & Mapper.xml에서 delete() 메서드 작성
@Override
public void remove(Long tno) {
todoMapper.delete(tno);
}
<delete id="delete">
delete from tbl_todo where tno=#{tno}
</delete>
(4) TodoControllder의 remove()메서드에서 TodoService와 연동
@PostMapping("/remove")
public String remove(Long tno, RedirectAttributes redirectAttributes){
log.info("-------------remove------------------");
log.info("tno: " + tno);
todoService.remove(tno);
return "redirect:/todo/list";
}
수정
스프링 MVC에서 지원하는 경로를 배열처럼 표기하는 표기법을 이용해 @Getmapping()인 read() 메서드로 목록을 받아 POST 방식 modify()로 기능 수행
- TodoMapper : 객체와 SQL을 매핑하는 클래스 -> 요청한 쿼리를 수행하는 XML
- TodoService : register()
- TodoController : urlpatterns -> @GetMapping, @PostMapping -> register()
- JSP : <form> -> submit
1. TodoMapper 개발
<update id="update">
update tbl_todo set title=#{title}, dueDate =#{dueDate}, finished=#{finished} where tno=#{tno}
</update>
@GetMapping({"/read", "/modify"})
public void read(Long tno, Model model){
TodoDTO todoDTO = todoService.getOne(tno);
log.info(todoDTO);
model.addAttribute("dto", todoDTO );
}
2. checkbox를 위한 Formatter
-수정 작업에서 화면의 체크박스로 완료여부 처리해야한다.
-브라우저는 체크박스가 클릭된 상태에서 전송되는 값은 'on'이라는 값을 전달한다.
(1) 컨트롤러에서 데이터를 수집할 때 타입을 변경하는 CheckBoxFormatter를 추가한다.
-'on'이라는 문자열을 boolean으로 처리한다.
package org.zerock.springex.controller.formatter;
import org.springframework.format.Formatter;
import java.text.ParseException;
import java.util.Locale;
public class CheckboxFormatter implements Formatter<Boolean> {
@Override
public Boolean parse(String text, Locale locale) throws ParseException {
if(text == null ) {
return false;
}
return text.equals("on");
}
@Override
public String print(Boolean object, Locale locale) {
return object.toString();
}
}
(2) servlet-context.xml에서 CheckboxFormatter를 등록
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="formatters">
<set>
<bean class="org.zerock.springex.controller.formatter.LocalDateFormatter"/>
<bean class="org.zerock.springex.controller.formatter.CheckboxFormatter"/>
</set>
</property>
</bean>
(3) TodoController의 POST방식의 modify() 작성
-@Valid : 필요한 내용 검증하는데, 문제가 발생하면 '/todo/modify'로 이동
-문제가 발생했을 때에도 tno 파라미터를 전달하기 위해 RedirectAttributes의 addAttribute() 메서드로 errors라는 이름으로 BindingResult의 모든 에러를 전달한다.
-미래 시간이 아닌 과거시간을 지정하는 거소가 같은 유효하지 않는 데이터 입력시 에러 발생해 같은 페이지로 무한 반복 이동된다.
@PostMapping("/modify")
public String modify(@Valid TodoDTO todoDTO,
BindingResult bindingResult,
RedirectAttributes redirectAttributes){
if(bindingResult.hasErrors()) {
log.info("has errors.......");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors() );
redirectAttributes.addAttribute("tno", todoDTO.getTno() );
return "redirect:/todo/modify";
}
log.info(todoDTO);
todoService.modify(todoDTO);
return "redirect:/todo/list";
}
문제 발생시 modify.jsp에서 수행하는 스크립트
-검증된 정보를 처리하는 코드
- <form>태그 끝난 후 <script> 태그로 @Valid에서 문제 발생시 자바스크립트 객체로 사용할 수 있도록 작성
<script>
const serverValidResult = {}
<c:forEach items="${errors}" var="error">
serverValidResult['${error.getField()}'] = '${error.defaultMessage}'
</c:forEach>
console.log(serverValidResult)
</script>
(4) 실제 'Modify' 버튼 이벤트 처리를 위해 <form>태그 전송
document.querySelector(".btn-primary").addEventListener("click",function(e) {
e.preventDefault()
e.stopPropagation()
formObj.action ="/todo/modify"
formObj.method ="post"
formObj.submit()
},false);
(5) 목록 버튼 클릭시 버튼 이벤트 처리
document.querySelector(".btn-secondary").addEventListener("click",function(e) {
e.preventDefault()
e.stopPropagation()
self.location = "/todo/list";
},false);
'Server Programming > Spring Boot Backend Programming' 카테고리의 다른 글
4장-4. 스프링 Web MVC 구현 (3) 검색과 필터링 조건 (+ 동적 쿼리, 쿼리 스트링, URLEncoder) (0) | 2022.12.02 |
---|---|
4장-3. 스프링 Web MVC 구현 (2) 페이징 처리 (+ 백틱) (0) | 2022.12.01 |
4장-1. 스프링과 스프링 Web MVC (1) | 2022.11.29 |
3장. 세션과 필터, 쿠키와 리스너 (+ 한글 깨짐 처리 / Optional<> / 옵저버 패턴) (0) | 2022.11.27 |
2장. 웹과 데이터베이스 (0) | 2022.11.25 |