본문 바로가기

Server Programming/Spring Boot Backend Programming

4장-2. 스프링 Web MVC 구현 (1) CRUD (+@Configuration, @Bean, 브라우저 한글 처리)

반응형

웹 MVC에서 스프링 프레임워크와 스프링 MVC으로의 이전

XML 설정 -> 어노테이션을 이용한 자바 설정

사용 기술

스프링 프레임워크

MyBatis

스프링 MVC

 

요구사항

CRUD

페이징

검색

 

  1. 검색과 필터링 가능한 화면
  2. MyBatis의 동적 쿼리을 이용해 해당하는 Todo검색
  3. 새로운 Todo 등록시 문자열, boolean, LocalDate를 자동으로 처리
  4. 목록에서 조회 화면 이동시 모든 검색, 필터링, 페이징 조건 유지
  5. 조회 화면에서 모든 조건 유지한 채로, 수정/삭제 화면으로 이동
  6. 삭제 후, 다시 목록 화면으로 이동 (PRG)
  7. 수정 후, 다시 조회 화면으로 이동하지만 검색, 필터링, 페이지 조건은 초기화 (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/

 

카드

여러 가지 종류와 옵션을 가진 유연하고 확장 가능한 콘텐츠를 제공합니다.

getbootstrap.kr

 

헤더와 푸터 기본 구조

<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/

 

내비게이션 바

Bootstrap의 강력하고 반응형적인 내비게이션 헤더, 내비게이션 바의 문서와 예. 콜랩스(collapse) 플러그인 지원을 포함한 브랜딩, 내비게이션 등의 지원이 포함되어 있습니다.

getbootstrap.kr

 

검색창이 없는 기본 네비게이션 구조

<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 개발 단계

  1. VO 선언
  2. Mapper 인터페이스 개발
  3. XML 개발
  4. 테스트 코드 개발

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 기능 개발

  • 등록
  • 목록
  • 조회
  • 수정/삭제
  • 검색
  • 페이징/필터링

 

등록

  1. TodoMapper : 객체와 SQL을 매핑하는 클래스 -> 요청한 쿼리를 수행하는 XML 
  2. TodoService : register()
  3. TodoController : urlpatterns -> @GetMapping, @PostMapping -> register()
  4. 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>

 

@ValidBindingResult 이용한 서버 사이드 검증

데이터 검증 이중 검증

  1. 브라우저의 프론트단 검증
  2. 서버 사이드에서 검증

의존성 추가

    //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";
    }
}

목록

-목록 데이터 출력 기능 부터 우선개발 후, 페이징 처리와 검색 기능 추가

 

  1. TodoMapper : 객체와 SQL을 매핑하는 클래스 -> 요청한 쿼리를 수행하는 XML 
  2. TodoService : register()
  3. TodoController : urlpatterns -> @GetMapping, @PostMapping -> register()
  4. 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를 통해 호출

 

  1. TodoMapper : 객체와 SQL을 매핑하는 클래스 -> 요청한 쿼리를 수행하는 XML 
  2. TodoService : register()
  3. TodoController : urlpatterns -> @GetMapping, @PostMapping -> register()
  4. 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 &nbsp;
                            </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 &nbsp;
                                </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()로 기능 수행

 

  1. TodoMapper : 객체와 SQL을 매핑하는 클래스 -> 요청한 쿼리를 수행하는 XML 
  2. TodoService : register()
  3. TodoController : urlpatterns -> @GetMapping, @PostMapping -> register()
  4. 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 파라미터를 전달하기 위해 RedirectAttributesaddAttribute() 메서드로 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);

 

 

 

반응형