1.  목록 데이터를 위한 DTO와 서비스 계층

🚀  TodoMapper에서 TodoVO의 목록과 전체 데이터의 수를 가져온다면 이를 서비스 계층에서 한 번에 담아서 처리하도록 DTO를 구성하는 것이 좋음
🚀  작성하려는 DTO는 PageResponseDTO라는 이름을 생성하고 다음과 같은 데이터와 가능을 가지도록 구성

  •  TodoDTO의 목록
  •  전체 데이터의 수
  •  페이지 번호 처리를 위한 데이터들 (시작 페이지 번호 / 끝 페이지 번호)

🚀  화면상에서 페이지 번호들을 출력하려면 현재 페이지 번호 page 와 페이지당 데이터의 수 size를 이용해서 계산할 필요가 있음
        ➡️  이 때문에 작성하려는 PageResponseDTO는 생성자를 통해서 필요한 page나 size 등을 전달받도록 구성

 

PageResponseDTO가 가져야 하는 데이터를 정리해서 클래스를 구성
public class PageResponseDTO<E> {

    private int page;
    private int size;
    private int total;

    // 시작 페이지 번호
    private int start;
    // 끝 페이지 번호
    private int end;

    // 이전 페이지의 존재 여부
    private boolean prev;
    // 다음 페이지의 존재 여부
    private boolean next;

    private List<E> dtoList;
}

 

  ✓  제네릭을 이용하는 이유는 나중에 다른 종류의 객체를 이용해서 PageResponseDTO를 구성할 수 있도록 하기 위함
       예를 들어 게시판이나 회원 정보 등도 페이징 처리가 필요할 수 있기 때문에 공통적인 처리를 위해서 제네릭으로 구성
 

   ✓  PageResponseDTO는 여러 정보를 생성자를 이용해서 받아서 처리하는 것이 안전
        예를 들어 PageRequestDTO 에 있는 page, size 값이 필요하고, TodoDTO 목록 데이터와 전체 데이터의 개수도 필요
 

PageResponseDTO의 생성자는 Lombok의 @Builder를 적용
public class PageResponseDTO<E> {
    @Builder(builderMethodName = "withAll")
    public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList, int total) {
        log.info(pageRequestDTO);
        this.page = pageRequestDTO.getPage();
        this.size = pageRequestDTO.getSize();

        this.total = total;
        this.dtoList = dtoList;
}

 


1) 페이지 번호 계산

페이지 번호를 계산하려면 우선 현재 페이지의 번호 page 가 필요.
화면에 10개의 페이지 번호를 출력한다고 했을 때 다음과 같은 경우들이 생길수 있음.

  •  현재 page가 1인 경우 : 시작 페이지 start는 1, 마지막 페이지 end는 10
  •  현재 page가 10인 경우 : 시작 페이지 start는 1, 마지막 페이지 end는 10
  •  현재 page가 11인 경우 : 시작 페이지 start는 11, 마지막 페이지 end는 20

 

A.  마지막 페이지 / 시작 페이지 번호의 계산


📍 end는 현재 페이지 번호를 기준으로 계산

this.end = (int)(Math.ceil(this.page/10.0)) * 10;

 

  👾  page를 10으로 나눈 값을 올림처리 한 후 * 10
         1 / 10 => 0.1 => 1 => 10
         11 / 10.0 => 1.1 => 2 => 20
         10 / 10 => 1.0 => 1 => 10

 

📍 시작 페이지 start의 경우 계산한 마지막 페이지에서 9를 빼면 됨

this.start = this.end - 9;

 

시작 페이지의 구성은 끝났지만 마지막 페이지의 경우 다시 전체 개수 total를 고려해야 함.
만일 10개씩 (size) 보여주는 경우 전체 개수 total가 75라면 마지막 페이지는 10이 아니라 8이 되어야 함.

int last = (int)(Math.ceil(total/(double)size));

 

     123 / 10.0 => 12.3 => 13
     100 / 10.0 => 10.0 => 10
     75 / 10.0 => 7.5 => 8

 

📍 마지막 페이지 end는 앞에서 구한 last 값보다 큰 경우에 last 값이 end가 되어야만 함

this.end = end > last ? last : end;

B. 이전 prev / 다음 next 의 계산


이전 prev 페이지의 존재 여부는 시작 페이지 start가 1이 아니라면 무조건 true
다음 next은 마지막 페이지 end 와 페이지당 개수 size를 곱한 값보다 전체 개수 total가 더 많은지 보고 판단

this.prev = this.start > 1;
this.next = total > this.end * this.size;

 

PageResponseDTO는 최종적으로 Lombok의 @Getter를 적용


2)  TodoService / TodoServiceImpl

TodoService와 TodoServiceImpl에서는 작성된 PageResponseDTO를 반환타입으로 지정해서 getList()를 구성

public interface TodoService {
    void register(TodoDTO todoDTO);

    //List<TodoDTO> getAll();

    PageResponseDTO<TodoDTO> getList(PageRequestDTO pageRequestDTO);

    TodoDTO getOne(Long tno);

    void remove(Long tno);

    void modify(TodoDTO todoDTO);
}
 @Override
    public PageResponseDTO<TodoDTO> getList(PageRequestDTO pageRequestDTO) {
        List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
        List<TodoDTO> dtoList = new ArrayList<>();
        for(TodoVO todoVO : voList) {
            dtoList.add(modelMapper.map(todoVO, TodoDTO.class));
        }

        int total = todoMapper.getCount(pageRequestDTO);

        PageResponseDTO<TodoDTO> pageResponseDTO = PageResponseDTO.<TodoDTO>withAll()
                .dtoList(dtoList)
                .total(total)
                .pageRequestDTO(pageRequestDTO)
                .build();
        return pageResponseDTO;
    }

 

TodoService의 getList()는 테스트를 통해서 결과를 확인
  @Test
    public void pageingTest() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder().page(1).size(10).build();
        PageResponseDTO<TodoDTO> pageResponseDTO = todoService.getList(pageRequestDTO);
        log.info(pageResponseDTO);
        for (TodoDTO todoDTO : pageResponseDTO.getDtoList()) {
            log.info(todoDTO);
        }
    }

 


3)  TodoController와 JSP처리

TodoController의 list()에서는 PageRequestDTO를 파라미터로 처리하고,

Model에 PageResponseDTO의 데이터를 담을 수 있도록 변경

@RequestMapping("/list")
    public void list(@Valid PageRequestDTO pageRequestDTO, BindingResult bindingResult, Model model) {
        log.info(pageRequestDTO);

        if(bindingResult.hasErrors()) {
            pageRequestDTO = pageRequestDTO.builder().build();
        }
        model.addAttribute("responseDTO", todoService.getList(pageRequestDTO));
    }

 

  ⚡️  list()는 @Valid를 이용해서 잘못된 파라미터 값들이 들어오는 경우 page는 1, size는 10으로 고정된 값을 처리하도록 구성
  ⚡️  기존과 달리 Model에 responseDTO라는 이름으로 PageResponseDTO를 담아 주었기 때문에 list.jsp를 수정

<c:forEach var="dto" items="${responseDTO.dtoList}">
    <tr>
        <th scope="row">${dto.tno}</th>
            <td><a href="/todo/read?tno=${dto.tno}" class="text-decoration-none">
                <c:out value="${dto.title}"/>
            </a></td>
            <td>${dto.writer}</td>
            <td>${dto.dueDate}</td>
            <td>${dto.finished}</td>
    </tr>
</c:forEach>

 

  ✓  프로젝트를 실행하고 브라우저에서 "/todo/list"를 호출했을 때 1페이지에 해당하는 데이터들이 출력되는 것을 확인


4)  페이지 이동 확인

화면을 추가로 개발하기 전에 "/todo/list?page=xx&size=xx"를 호출해서 결과가 정상적으로 처리되는지 확인.
page의 경우는 음수가 될 수 없고, size는 100을 넘을 수 없음.


A.  화면에 페이지 이동을 위한 번호 출력


브라우저를 통해서 페이지의 이동에 문제가 없다는 것을 확인했다면 화면 아래쪽에 페이지 번호들을 출력하도록 구성.
페이지 번호는 부트스트랩의 pagination이라는 컴포넌트를 적용.

 

list.jsp에 <table>태그가 끝난 후에 <div>를 구성해서 화면에 적용
<div class="float-end">
    <ul class="pagination flex-wrap">
        <c:forEach var="num" begin="${responseDTO.start}" end="${responseDTO.end}">
            <li class="page-item"><a class="page-link" href="#">${num}</a></li>
        </c:forEach>
    </ul>    
</div>

 

  ✓  브라우저에서 page를 변경시켜 페이지 번호들이 변경되는지 확인

 


B.  화면에서 prev / next / 현재 페이지


페이지 번호들이 정상적으로 출력된다면 "이전 / 다음"을 처리

<div class="float-end">
    <ul class="pagination flex-wrap">
        <c:if test="${responseDTO.prev}">
            <li class="page-item"><a class="page-link">Previous</a></li>
        </c:if>
        
        <c:forEach var="num" begin="${responseDTO.start}" end="${responseDTO.end}">
            <li class="page-item"><a class="page-link" href="#">${num}</a></li>
        </c:forEach>
                                
        <c:if test="${responseDTO.next}">
            <li class="page-item"><a class="page-link">Next</a></li>
        </c:if>
    </ul>
</div>

 

  ⚡️  11페이지 이상되었을 때 Previous 버튼이 보임

  ⚡️  1 ~ 10 페이지의 경우 Previous 버튼이 보이지 않음

📍 현재 페이지의 번호는 class 속성에 "active"라는 속성값이 추가되어야 함. 삼항 연산자를 이용

<c:forEach var="num" begin="${responseDTO.start}" end="${responseDTO.end}">
    <li class="page-item" ${responseDTO.page == num ? "active" : ""}>
    <a class="page-link" href="#">${num}</a></li>
</c:forEach>

 

 


C.  페이지의 이벤트 처리


👾  화면에서 페이지 번호를 누르면 이동하는 처리는 자바스크립트를 이용해서 처리
👾  화면의 페이지 번호를 의미하는 <a> 태그에 직접 "onclick"을 적용할 수도 있지만, 한 번에 <ul> 태그에 이벤트를 이용해서 처리
👾  우선은 각 페이지 번호에 적절한 페이지 번호를 가지도록 구성. 이때는 "data-"속성을 이용해서 필요한 속성을 추가해주는 방식을 사용

 

 "data-num"이라는 속성을 추가해서 페이지 번호를 보관하도록 구성
<c:if test="${responseDTO.prev}">
    <li class="page-item">
        <a class="page-link" data-num="${reponseDTO.start-1}">Previous</a>
    </li>
</c:if>

<c:forEach var="num" begin="${responseDTO.start}" end="${responseDTO.end}">
    <li class="page-item" ${responseDTO.page == num ? "active" : ""}>
    <a class="page-link" data-num="${num}">${num}</a></li>
</c:forEach>

<c:if test="${responseDTO.next}">
    <li class="page-item">
    <a class="page-link" data-num="${responseDTO.end+1}">Next</a></li>
</c:if>

 

<ul> 태그가 끝난 부분에 이벤트 처리 추가
 <script>
    document.querySelector(`.pagination`).addEventListener('click', function (e) {
        e.preventDefault();
        e.stopPropagation();

        const target = e.target;
        if(target.tagName !== 'A') {
            return;
        }
        const num = target.getAttribute('data-num');
        self.location = `/todo/list?page=\${num}`; 
    });
</script>

 

  ⚡️  자바 스크립트의 이벤트 처리는 <ul> 태그에 이벤트를 등록하고 <a> 태그를 클릭했을 때만 data-num 속성값을 읽어와서 현재 주소(self.location)를 변경하는 방식으로 작성
  ⚡️  자바 스크립트에서 백틱(`)을 이용하면 문자열 결합에 '+'를 이용해야 하는 불편함을 줄일 수 있음. 대신에 JSP의 EL이 아니라는 것을 표시하기 위하여 \${}로 처리
  ⚡️  자바 스크립트 처리가 완료되면 화면상의 페이지 번호를 클릭해서 페이지 이동이 가능한지 확인

 


D.  조회 페이지로 이동


목록 페이지는 특정한 Todo의 제목 title을 눌러서 조회 페이지로 이동하는 기능이 존재
기존에는 단순히 tno만을 전달해서 '/todo/read?tno=33'과 같은 방식으로 이동했지만, 페이지 번호가 붙을 때는 page와 size 등을 같이 전달해 주어야만 조회 페이지에서 다시 목록으로 이동할 때 기존 페이지을 볼 수 있게 됨
이를 위해 list.jsp에는 각 Todo의 링크 처리 부분을 수정할 필요가 있음

 

페이지 이동 정보는 PageRequestDTO 안에 있으므로 PageRequestDTO 내부에 간단한 메서드를 작성해서 필요한 링크를 생성해서 사용. (파라미터로 전달되는 PageRequestDTO는 Model로 자동 전달되기 때문에 별도의 처리가 필요하지 않음)

 

PageRequestDTO에 link라는 속성을 추가하고 getLink()를 추가해서 GET 방식으로 페이지 이동에 필요한 링크들을 생성
private String link;

public String getLink() {
        if (link == null) {
            StringBuilder builder = new StringBuilder();
            builder.append("page=" + this.page);
            builder.append("&size=" + this.size);
            link = builder.toString();
        }
        return link;
}

 

list.jsp 수정
<c:forEach var="dto" items="${responseDTO.dtoList}">
    <tr>
        <th scope="row">${dto.tno}</th>
        <td><a href="/todo/read?tno=${dto.tno}&${pageRequestDTO.link}" 
            class="text-decoration-none">
            <c:out value="${dto.title}"/>
        </a></td>
        <td>${dto.writer}</td>
        <td>${dto.dueDate}</td>
        <td>${dto.finished}</td>
    </tr>
</c:forEach>

 

  ⚡️ 기존의 tno=xx 뒤에 &를 추가하고 PageRequestDTO의 getLink()의 결과인 문자열을 생성하게 되면 다음과 같이 기존 링크에
page와 size가 추가된 형태가 됨

<a href="/todo/read?tno-3579&page=1&size=10" class="text-decoration-none" data-tno=357>

 


E. 조회에서 목록으로

 

조회 화면에서는 기존과 달리 PageRequestDTO를 추가로 이용하도록 TodoController를 수정

 

TodoController의 read 메소드는 PageRequestDTO 파라미터를 추가해서 수정
 @GetMapping({"/read", "/modify"})
    public void read(Long tno, PageRequestDTO pageRequestDTO, Model model) {
        TodoDTO todoDTO = todoService.getOne(tno);
        log.info(todoDTO);
        model.addAttribute("dto", todoDTO);
    }

 

read.jsp에서는 List 버튼의 링크를 다시 처리
document.querySelector('.btn-secondary').addEventListener('click', function (e) {
    self.location = "/todo/list?${pageRequestDTO.link}";
}, false);

 

  👾  브라우저는 특정한 페이지에서 조회 페이지로 이동해서 List 버튼을 눌렀을 때 정상적으로 이동하는지 확인

 


F.  조회에서 수정으로


조회 화면에서 수정 화면으로 이동할 때도 현재 페이지 정보를 유지해야 하기 때문에 read.jsp에서는 링크 처리 부분을 수정

document.querySelector('.btn-primary').addEventListener('click', function (e) {
    self.location = `/todo/modify?tno=${dto.tno}&${pageRequestDTO.link}`;
}, false);

 

  👾  화면에서 Modify 버튼을 누르면 아래와 같이 동작

 


G.  수정화면에서의 링크 처리


수정 화면에서도 다시 목록으로 가는 링크가 필요. TodoController의 read() 메서드는 GET 방식으로 동작하는 '/todo/modify'에도 동일하게 처리되므로 JSP에서 PageRequestDTO를 사용할 수 있음.

 

modify.jsp 의 List 버튼을 누르는 자바 스크립트의 이벤트 부분을 변경
document.querySelector('.btn-secondary').addEventListener('click', function (e) {
    self.location = `/todo/list?${pageRequestDTO.link}`;
});

 


H.  수정 / 삭제 처리 후 페이지 이동


실제 수정 / 삭제 작업은 POST 방식으로 처리되고 삭제 처리가 된 후에는 다시 목록으로 이동할 필요가 있음.
그렇기 때문에 수정 화면에서 <form> 태그로 데이터를 전송할 때 페이지와 관련된 정보를 같이 추가해서 전달해야 함.

 

modify.jsp에 <input type="hidden">을 이용해서 추가
<form action="/todo/modify" method="post">
    <input type="hidden" name="page" value="${pageRequestDTO.page}">
    <input type="hidden" name="size" value="${pageRequestDTO.size}">

 

 

TodoController에서 POST 방식으로 이루어지는 삭제 처리에도 PageRequestDTO를 이용해서 <form> 태그로 전송되는 태그들을 수집하고 수정 후에 목록 페이지로 이동할 때 page는 무조건 1페이지로 이동해서 size 정보를 활용

@PostMapping("/remove")
    public String remove(Long tno, PageRequestDTO pageRequestDTO , RedirectAttributes redirectAttributes) {
        log.info("-----remove----");
        log.info("tno: " + tno);

        todoService.remove(tno);
        redirectAttributes.addAttribute("page", 1);
        redirectAttributes.addAttribute("size", pageRequestDTO.getSize());
        return "redirect:/todo/list";
    }

 

  👾  브라우저는 목록에서 특정한 Todo를 조회 -> 수정 / 삭제 화면 -> 삭제 후 이동이 정상적으로 이루어지는지 확인

 


I.  수정 처리 후 이동


Todo를 수정한 후에 목록으로 이동할 때는 페이지 정보를 이용해야 하므로 TodoController의 modify() 에서는 PageRequestDTO를 받아서 처리하도록 변경.

    @PostMapping("/modify")
    public String midify(PageRequestDTO pageRequestDTO,
                         @Valid TodoDTO todoDTO,
                         BindingResult bindingResult,
                         RedirectAttributes redirectAttributes) {
        if (bindingResult.hasErrors()) {
            log.info("has error");
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            redirectAttributes.addAttribute("tno", todoDTO.getTno());
            return "redirect:/todo/mofidy";
        }
        log.info(todoDTO);
        todoService.modify(todoDTO);
        
        redirectAttributes.addAttribute("page", pageRequestDTO.getPage());
        redirectAttributes.addAttribute("size", pageRequestDTO.getSize());
        
        return "redirect:/todo/list";
    }

 

  👾  정상적으로 수정된 후에는 '/todo/list'로 이동할 때 필요한 page나 size를 유지할 수 있도록 구성

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  페이징 처리를 위한 TodoMapper

👩🏻‍💻  Todo 데이터의 수가 많아진다면 데이터베이스에서 많은 시간이 걸릴 수 있고, 화면에서 이를 출력하는 데도 많은 시간과 자원이 소모
👩🏻‍💻  일반적으로는 많은 데이터를 보여주는 작업은 페이징 처리를 해서 최소한의 데이터들을 보여주는 방식을 선호
        ⚡️  페이징 처리를 하게 되면 데이터베이스에서 필요한 만큼의 최소한의 데이터를 가져오고 이를 출력하기 때문에 성능 개선에도 많은 도움이 됨

 

1)  페이징을 위한 SQL 연습

개발 전에 데이터베이스 상에서 원하는 동작을 미리 구현.


A. 더미 데이터 추가하기

 

많은 양의 데이터를 만드는 방법. 2배씩 늘어나기 때문에 몇번만 실행하면 많은 데이터를 생성함.
더미 데이터 추가를 위해 아래의 SQL을 여러번 처리.

insert into tbl_todo(title,dueDate,writer) (select title,dueDate,writer from tbl_todo);

 

  📍 최종적으로 데이터의 수를 확인

select count(tno) from tbl_todo;

B.  limit 실습


페이징 처리를 위해서는 select의 마지막 부분에는 limit 처리를 사용.
일반적으로 웹에서는 가장 최근에 등록된 데이터를 우선으로 보여주므로 가장 마지막에 등록된 데이터가 순차적으로 보이도록 쿼리를 작성

select * from tbl_todo order by tno desc;

 

📍  limit 뒤의 값이 한 개만 존재하는 경우 가져와야 하는 데이터의 수를 의미. 가장 최근 데이터 10개를 가져옴

select*from tbl_todo order by tno desc limit 10;
select*from tbl_todo order by tno desc limit 0, 10;

 


C.  count()의 필요성


페이징 처리를 하기 위해서는 페이지 번호를 구성할 때 필요한 전체 데이터의 개수가 필요함.
예를 들어 전체 데이터가 30개이면 3 페이지까지만 출력해야 하는 작업에서 사용

select count(tno) from tbl_todo;

 


2)  페이지 처리를 위한 DTO

💡  페이지 처리는 현재 페이지 번호 page, 한 페이지당 보여주는 데이터의 수 size가 기본적으로 필요

      ➡️  2개의 숫자를 매번 전달할 수도 있겠지만 나중에 확장 여부를 고려해서라도 별도의 DTO를 만들어 두는 것이 좋음

 

dto 패키지에 PageRequestDTO 클래스를 정의
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {
    @Builder.Default
    @Min(value = 1)
    @Positive
    private int page = 1;

    @Builder.Default
    @Min(value=10)
    @Max(value=100)
    @Positive
    private int size = 10;

    public int getSkip() {
        return (page - 1) * size;
    }

}

 

  ✏️  페이지 번호 page와 한 페이지당 개수 size를 보관하는 용도 외에도 limit에서 사용하는 건너뛰기 skip 의 수를 getSkip() 만들어서 사용
  ✏️  page나 size는 기본값을 가지기 위해서 Lombok의 @Builder.Default를 이용
  ✏️  @Min, @Max를 이용해서 외부에서 조작하는 것에 대해서도 대비하도록 구성

 


3)  TodoMapper의 목록 처리

TodoMapper 인터페이스는 PageRequestDTO를 파라미터로 처리하는 selectList()를 추가

 List<TodoVO> selectList(PageRequestDTO pageRequestDTO);

 

TodoMapper.xml 내에서 selectList는 다음과 같이 구현
    <select id="selectList" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
        select * from tbl_todo order by tno desc limit #{skip}, #{size}
    </select>

 

  ✏️  MyBatis는 기본적으로 getXXX, setXXX를 통해서 동작하기 때문에 #{skip}의 경우는 getSkip()을 호출
 

테스트 코드를 이용해서 TodoMapper의 selectList()가 정상적으로 동작하는지 확인
 @Test
    public void testSelectList() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .page(5)
                .build();
        List<TodoVO> todoVOList = todoMapper.selectList(pageRequestDTO);
        for (TodoVO todoVO : todoVOList) {
            log.info(todoVO);
        }

        todoVOList.forEach(item -> log.info(item));

    }

 


4)  TodoMapper의 count 처리

화면에 페이지 번호들을 구성하기 위해서는 전체 데이터의 수를 알아야만 가능
  ✓ 예를 들어 마지막 페이지가 7에서 끝나야 하는 상황이 생긴다면 화면상에서도 페이지 번호를 조정해야 하기 때문

 

TodoMapper에 getCount()를 추가


  👾  getCount()는 나중에 검색에 대비해서 PageRequestDTO를 파라미터로 받로록 설계

 int getCount(PageRequestDTO pageRequestDTO);

 

TodoMapper.xml은 우선은 전체 개수를 반환하도록 구성
<select id="getCount" resultType="int">
    select COUNT(*) FROM tbl_todo
</select>

 

test 코드 실행 확인
 @Test
    public void testGetCount() {
        log.info(todoMapper.getCount(PageRequestDTO.builder().build()));
    }

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  Todo의 삭제 기능 개발

수정과 삭제는 GET 방식으로 조회한 후에 POST 방식으로 처리
사실상 GET 방식의 내용은 조회 화면과 같지만 스프링 MVC에는 여러 개의 경로를 배열과 같은 표기법을 이용해서 하나의 @GetMapping으로 처리할 수 있기 때문에 read() 기능을 수정해서 수정과 삭제에도 같은 메서드를 이용하도록 작업

 

TodoController의 read()를 다음과 같이 수정해서 "/todo/modify?tno=xxx"의 경로를 처리하도록 수정
 @GetMapping({"/read", "/modify"})
    public void read(Long tno, Model model) {
        // 1) request로 전달 받은 tno를 서비스에 전달해서 2)TodoDTO를 반환받아서 3)View 에 전달
        TodoDTO todoDTO = todoService.getOne(tno);
        log.info(todoDTO);
        model.addAttribute("dto", todoDTO);
    }

 

 

/WEB-INF/view/todo 폴더에 있는 read.jsp를 그대로 복사해서 modify.jsp를 구성


  👾  modify.jsp에서는 수정과 삭제 작업이 POST 방식으로 처리될 예정이므로 이를 위한 <form> 태그를 구성하고 수정이 가능한 항목들은 편집이 가능하도록 함

<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="${dto.tno}" readonly>
    </div>
    <div class="input-group mb-3">
        <span class="input-group-text">Title</span>
        <input type="text" name="title" class="form-control" value="${dto.title}">
    </div>
    <div class="input-group mb-3">
        <span class="input-group-text">DueDate</span>
        <input type="date" name="dueDate" class="form-control" value="${dto.dueDate}">
    </div>
    <div class="input-group mb-3">
        <span class="input-group-text">Writer</span>
        <input type="text" name="writer" class="form-control" value="${dto.writer}" readonly>
    </div>
    <div class="form-check">
        <label class="form-check-label">
            Finished &nbsp;
        </label>
        <input type="checkbox" name="finished"
            class="form-check-input" ${dto.finished ? "checked" : ""}>
    </div>
    <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>
</form>

 

 

 

 ✓  제목 / 만료일 / 완료는 수정이 가능하도록 수정
  ✓  화면 아래 버튼은 삭제, 수정, 목록 버튼이 추가
  ✓  브라우저를 통해 "/todo/modify?tno=xx"와 같은 경로로 화면이 나오는 것 확인

 

 

 

 

 

 

 


 

1)  Remove 버튼의 처리


Remove 버튼의 클릭은 자바 스크립트를 이용해서 <form> 태그의 action을 조정하는 방식으로 동작하게 구성

<script>
    const frmModify = document.querySelector('form');
    
    document.querySelector('.btn-danger').addEventListener('click', function () {
        frmModify.action = '/todo/remove';
        frmModify.method = 'post';
        frmModify.submit();
    });
</script>

 

  ✓  Remove 버튼은 class 속성이 "btn-danger"이므로 이를 이용해서 클릭 이벤트를 처리

 

TodoController에는 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";
    }

 

  ✓  우선 tno 파라미터가 정상적으로 전달되는지 확인하고 목록으로 이동하도록 구성

  ✓  Remove 버튼을 누르면 다음과 같은 로그 출력 확인


 

2) TodoMapper와 TodoService 처리

TodoMapper에는 delete() 메서드를 추가하고 TodoMapper.xml에는 sql을 추가
public interface TodoMapper {
    String getTime();

    void insert(TodoVO todoVO);

    List<TodoVO> selectAll();

    TodoVO selectOne(Long tno);

    void delete(Long tno);
}
    <delete id="delete">
        DELETE FROM tbl_todo WHERE tno = #{tno}
    </delete>

 

test 코드 작성
  @Test
    public void testDelete() {
        // 1) tno로 데이터를 반환해서 정상 출력 확인 2) 삭제 3) 다시 tno로 데이터를 반환해서 삭제 확인.
        Long tno = 2L;
        TodoVO todoVO = todoMapper.selectOne(tno);
        log.info(todoVO);

        todoMapper.delete(tno);

        todoVO = todoMapper.selectOne(tno);
        log.info(todoVO);

    }

 

TodoServce / TodoServiceImpl에는 remove() 메서드를 작성
public interface TodoService {
    void register(TodoDTO todoDTO);

    List<TodoDTO> getAll();

    TodoDTO getOne(Long tno);

    void remove(Long tno);
}
    @Override
    public void remove(Long tno) {
        todoMapper.delete(tno);
    }

 

 

TodoController 에서 TodoService의 remove()를 호출하는 코드를 추가
    @PostMapping("/remove")
    public String remove(Long tno, RedirectAttributes redirectAttributes) {
        log.info("-----remove----");
        log.info("tno: " + tno);

        todoService.remove(tno);
        return "redirect:/todo/list";
    }

 

  ✓  브라우저를 통해서 특정한 번호를 가진 게시물이 삭제 되는지 확인

 


2.  Todo의 수정 기능 개발

Todo의 수정 기능은 수정이 가능한 항목들만 변경되어야 하므로 SQL이 복잡해짐

 

TodoMapper에 메서드 추가
public interface TodoMapper {
    String getTime();

    void insert(TodoVO todoVO);

    List<TodoVO> selectAll();

    TodoVO selectOne(Long tno);

    void delete(Long tno);

    void update(TodoVO todoVO);
}

 

TodoMapper.xml 코드 추가
<update id="update">
    UPDATE tbl_todo SET title = #{title}, dueDate = #{dueDate}, 
        finished = #{finished} WHERE tno = #{tno}
</update>

 

test 코드 추가
@Test
    public void testUpdate() {
        Long tno = 12L;
        TodoVO todoVO = TodoVO.builder()
                .tno(tno)
                .title("수정")
                .dueDate(LocalDate.parse("2024-4-26"))
                .finished(true)
                .build();

        todoMapper.update(todoVO);
        log.info(todoMapper.selectOne(tno));
    }

 

 

TodoService / TodoServiceImpl 에서는 TodoDTO를 TodoVO로 변환해서 처리
public interface TodoService {
    void register(TodoDTO todoDTO);

    List<TodoDTO> getAll();

    TodoDTO getOne(Long tno);

    void remove(Long tno);

    void modify(TodoDTO todoDTO);
}
 @Override
    public void modify(TodoDTO todoDTO) {
        log.info("...modify()...");

        TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);
        log.info(todoVO);
        todoMapper.update(todoVO);
    }

 


1)  checkbox를 위한 Formatter


👾  수정 작업에서는 화면에서 체크박스를 이용해서 완료여부 finished (boolean) 를 처리하게 됨
👾  문제는 브라우저가 체크박스가 클릭된 상태일때 전송되는 값은 "on"이라는 값을 전달
👾  TodoDTO로 데이터를 수집할 때에는 문자열 "on"을 boolean 타입으로 처리할 수 있어야 하므로 컨트롤러에서 데이터를 수집할 때 타입을 변경해 주기 위한 CheckboxFormatter 를 formatter 패키지에 추가해서 개발

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();
    }
}

 

servlet-context.xml에 format 라이브러리 추가
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
        <set>
            <bean class="com.example.spring_ex_01_2404.controller.formatter.LocalDateFormatter"/>
            <bean class="com.example.spring_ex_01_2404.controller.formatter.CheckBoxFormatter"/>
        </set>
    </property>
</bean>

 


2)  TodoController의 modify()

@PostMapping("/modify")
    public String midify(@Valid TodoDTO todoDTO,
                         BindingResult bindingResult,
                         RedirectAttributes redirectAttributes) {
        if (bindingResult.hasErrors()) {
        // 유효성 검사 결과 에러가 있으면 수정 페이지로 돌아감
            log.info("has error");
            
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            redirectAttributes.addAttribute("tno", todoDTO.getTno());
            return "redirect:/todo/mofidy";
        }
        log.info(todoDTO);
        todoService.modify(todoDTO);
        return "redirect:/todo/list";
    }

 

  ✓  @Valid를 이용해서 필요한 내용들을 검증하고 문제가 있는 경우에는 다시 "/todo/modify"로 이동시키는 방식을 사용
  ✓  "/todo/modify"로 이동할 때는 tno 파라미터가 필요하기 때문에 RedirectAttributes의 addAttribute을 이용

 

/WEB-INF/view/todo/modify.jsp 에 검증된 정보를 처리하는 코드를 추가


  👾  <form> 태그가 끝난 후에는 <script> 태그를 이용

<script>
    const serverValidResult = {};
    <c:forEach items="${errors}" var="error">
    serverValidResult['${error.getField()}'] = '${error.defaultMessage}';
    </c:forEach>
    console.log(serverValidResult);
</script>
<script>
    const frmModify = document.querySelector('form');
                             
    document.querySelector('.btn-danger').addEventListener('click', function () {
        frmModify.action = '/todo/remove';
        frmModify.method = 'post';
        frmModify.submit();
    });
                            
    document.querySelector('.btn-primary').addEventListener('click', function () {
        frmModify.action = '/todo/modify';
        frmModify.method = 'post';
        frmModify.submit();
    });
                           
    document.querySelector('.btn-secondary').addEventListener('click', function () {
        self.location = '/todo/list';
    });
</script>

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  Todo 목록 기능 개발

등록 기능의 개발이 완료된 후 결과 화면은 목록으로 이동
목록의 경우 나중에 페이징 처리나 검색 기능이 필요하지만 시작하는 단계에서는 목록 데이터를 출력하는 수준으로 작성

 

1) TodoMapper의 개발

TodoMapper 인터페이스에 가장 최근에 등록된 TodoVO가 우선적으로 나올 수 있도록 selectAll()를 추가
public interface TodoMapper {
    String getTime();

    void insert(TodoVO todoVO);

    List<TodoVO> selectAll();
}

 

 

TodoMapper.xml에서는 selectAll()의 실제 쿼리문을 작성


  👾  <select> 태그의 경우 resultType을 지정하는 것에 주의
          ➡️  resultType은 JDBC의 ResultSet의 한 행 row을 어떤 타입의 객체로 만들것인지를 지정

    <select id="selectAll" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
        SELECT * FROM tbl_todo ORDER BY tno DESC
    </select>

 

마지막으로 test 폴더 내에 작성해둔 TodoMapperTests 클래스를 이용해 테스트 코드를 작성
@Test
    public void testSelectAll() {
        List<TodoVO> todoVOList = todoMapper.selectAll();
        for (TodoVO todoVO : todoVOList) {
            log.info(todoVO);
        }

        todoVOList.forEach(item -> log.info(item));
        // for문과 foreach문 둘 중 하나 사용
    }

 

  ✓  테스트 실행 결과는 가장 나중에 추가된 데이터를 우선적으로 보여줌


2) TodoService / TodoServiceImpl의 개발

서비스 계층의 개발은 특별한 파라미터가 없는 경우 TodoMapper을 호출하는것이 전부이다. 다만, TodoMapper가 반환하는 데이터의 타입이 List<TodoVO>이기 때문에 이를 List<TodoDTO>로 변환하는 작업이 필요.

 

TodoService 인터페이스에 getAll() 기능을 추가
public interface TodoService {
    void register(TodoDTO todoDTO);

    List<TodoDTO> getAll();
}

 

TodoServiceImpl에서 getAll()을 작성

 

    @Override
    public List<TodoDTO> getAll() {
        List<TodoVO> voList = todoMapper.selectAll(); // dao에서 데이터베이스에서 들고온 VO리스트를 리턴
        List<TodoDTO> dtoList = new ArrayList<>();
        for (TodoVO todoVO: voList) {
            // 개별 VO를 DTO로 변환.
            TodoDTO todoDTO = modelMapper.map(todoVO, TodoDTO.class);
            dtoList.add(todoDTO); // DTO리스트에 저장.
        }
        return dtoList;
    }

 

💡  List<TodoVO>를 List<TodoDTO>로 변화하는 작업은 java8부터 지원하는 stream을 이용해서 각 TodoVO는 map()을 통해서 TodoDTO로 바꾸고, collect()를 이용해서 List로 묶어 줌

 

test 코드 작성후 실행
@Test
    public void testGetAll() {
        List<TodoDTO> todoDTOList = todoService.getAll();
        for (TodoDTO todoDTO: todoDTOList) {
            log.info(todoDTO);
        }

        todoDTOList.forEach(item -> log.info(item));
        // 둘 중 하나 사용
    }

 

 


3)  TodoController의 처리

TodoController의 list() 기능에서 TodoService를 처리하고 Model에 데이터를 담아서 JSP로 전달
@RequestMapping("/list")
    public void list(Model model) {
        log.info("todo list...");
        model.addAttribute("dtoList", todoService.getAll());
    }

 

  ✓  Model에는 dtoList라는 이름으로 목록 데이터를 담았기 때문에 JSP에서는 JSTL을 이용해서 목록을 출력

  ✓  화면 디자인은 부트스트랩의 tables 항목 참고

 

/WEB-INF/view/todo/list.jsp 페이지 생성

 

  • test.html 을 복사후 상단에 JSP, JSTL 설정 추가
  • <div class="card-body"> 부분을 아래와 같이 수정
<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 var="dto" items="${dtoList}">
            <tr>
                <th scope="row">${dto.tno}</th>
                <td>${dto.title}</td>
                <td>${dto.writer}</td>
                <td>${dto.dueDate}</td>
                <td>${dto.finished}</td>
            </tr>
        </c:forEach>
        </tbody>
    </table>
</div>

 


2.  Todo 조회 기능 개발

목록 화면에서 제목을 클릭했을때 "/todo/read?tno=xx"와 같이 TodoController를 호출하도록 개발

 

1)  TodoMapper 조회 기능 개발

 

TodoMapper의 개발은 selectOne()이라는 메소드를 추가


  👾  파라미터는 Long 타입으로 tno를 받도록 설계하고, TodoVO객체를 반환하도록 구성

public interface TodoMapper {
    String getTime();

    void insert(TodoVO todoVO);

    List<TodoVO> selectAll();

    TodoVO selectOne(Long tno);
}

 

TodoMapper.xml에 selectOne 추가
<select id="selectOne" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
    SELECT * FROM tbl_todo WHERE tno = #{tno}
</select>

 

test 코드 작성
@Test
    public void testSelectOne() {
        TodoVO todoVO = todoMapper.selectOne(2L);
        log.info(todoVO);
    }

 


2)  TodoService / TodoServiceImpl의 개발

 

public interface TodoService {
    void register(TodoDTO todoDTO);

    List<TodoDTO> getAll();

    TodoDTO getOne(Long tno);
}
@Override
    public TodoDTO getOne(Long tno) {
        TodoVO todoVO = todoMapper.selectOne(tno);
        TodoDTO todoDTO = modelMapper.map(todoVO, TodoDTO.class);
        return todoDTO;
    }

 


3) TodoController의 개발


GET방식으로 동작하는 read() 기능을 개발

 @GetMapping("/read")
    public void read(Long tno, Model model) {
        // 1) request로 전달 받은 tno를 서비스에 전달해서 2)TodoDTO를 반환받아서 3)View 에 전달
        TodoDTO todoDTO = todoService.getOne(tno);
        log.info(todoDTO);
        model.addAttribute("dto", todoDTO);
    }

 

read.jsp 파일 생성
<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="${dto.tno}" readonly>
    </div>
    <div class="input-group mb-3">
        <span class="input-group-text">Title</span>
        <input type="text" name="title" class="form-control" value="${dto.title}" readonly>
    </div>
    <div class="input-group mb-3">
        <span class="input-group-text">DueDate</span>
        <input type="date" name="dueDate" class="form-control" value="${dto.dueDate}" readonly>
    </div>
    <div class="input-group mb-3">
        <span class="input-group-text">Writer</span>
        <input type="text" name="writer" class="form-control" value="${dto.writer}" readonly>
    </div>
                        
    <div class="form-check">
       <label class="form-check-label">
           Finished &nbsp;
       </label>
       <input type="checkbox" name="finished" class="form-check-input" ${dto.finished ? "checked" : ""} disabled>
    </div>
    <div class="my-4">
        <div class="float-end">
             <button type="submit" class="btn btn-primary">Modify</button>
             <button type="reset" class="btn btn-secondary">List</button>
        </div>
    </div>
</div>


4)  수정 / 삭제를 위한 링크 처리


조회 화면에 수정 / 삭제를 위해서 Modify 버튼을 클릭하면 GET 방식의 수정 / 삭제 선택이 가능한 화면으로 이동
  ➡️  자바 스크립트를 이용해서 이벤트 처리

read.jsp 의 card-body 클래스 아래에 코드 작성
<script>
    document.querySelector('.btn-primary').addEventListener('click', function () {
        self.location = '/todo/modify?tno=' + ${dto.tno};
    });
    document.querySelector('.btn-secondary').addEventListener('click', function () {
        self.location = '/todo/list';
    })
</script>

 

list.jsp의 링크 처리
<tbody>
    <c:forEach var="dto" items="${responseDTO.dtoList}">
    <tr>
        <th scope="row">${dto.tno}</th>
        <td><a href="/todo/read?tno=${dto.tno}" class="text-decoration-none">
            ${dto.title}</a></td>
        <td>${dto.writer}</td>
        <td>${dto.dueDate}</td>
        <td>${dto.finished}</td>
    </tr>
    </c:forEach>
</tbody>

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  Todo 기능 개발

등록 작업의 경우 TodoMapper ▶️ TodoService ▶️ TodoController ▶️ JSP의 순서로 처리


1) TodoMapper 개발 및 테스트

 

TodoMapper에는 TodoVO를 파라미터로 받는 insert()를 추가
public interface TodoMapper {

    String getTime();

    void insert(TodoVO todoVO);
    
}

 

TodoMapper.xml에 insert를 다음과 같이 구현
<insert id="insert">
        INSERT INTO tbl_todo (title, dueDate, writer) VALUES (#{title}, #{dueDate}, #{writer})
</insert>

 

Mybatis를 이용하면 "?" 대신에 "#{title}" 같이 파라미터를 처리, "#{title}" 부분은 PreparedStatement로 변경되면서 "?"로 처리되고, 주어진 객체의 getTitle()을 호출한 결과를 적용

 

테스트 코드를 이용해서 TodoVO의 입력을 확인
 @Test
    public void testInsert() {
        TodoVO todoVO = TodoVO.builder() // 빌더를 이용해서 TodoVO 객체를 생성
                .title("스프링 테스트")
                .dueDate(LocalDate.of(2022, 10, 10))
                .writer("user00")
                .build();
        todoMapper.insert(todoVO);
    }

 

  테스트 실행 후에 tbl_todo 테이블을 조회해서 insert가 완료되었는지 확인

 


2) TodoService와 TodoServiceImpl 클래스


TodoMapper와 TodoController 사이에는 서비스 계층을 설계해서 적용
TodoService 인터페이스를 먼저 추가하고, 이를 구현한 TodoServiceImpl을 스프링 빈으로 처리

public interface TodoService {
    void register(TodoDTO todoDTO);
}

 

  - TodoService 인터페이스에 추가한 register()는 여러 개의 파라미터 대신에 TodoDTO로 묶어서 전달 받도록 구성

 

 

TodoService 인터페이스를 구현하는 TodoServiceImpl에는 의존성 주입을 이용해서 데이터베이스 처리를 하는 TodoMapper와 DTO, VO의 변환을 처리하는 ModelMapper를 주입

 

    ⚡️  의존성 주입이 사용되는 방식은 의존성 주입이 필요한 객체의 타입을 final로 고정하고 @RequiredArgsConstructor를 이용해서 생성자를 생성하는 방식을 사용

    ⚡️  register() 에서는 주입된 ModelMapper를 이용해서 TodoDTO를 TodoVO로 변환하고 이를 TodoMapper를 통해서 insert 처리

@Log4j2
@Service
@RequiredArgsConstructor // 생성자 객체 주입. private final로 선언된 참조변수에 객체를 저장하는 생성자 작성.
public class TodoServiceImpl implements TodoService{
    private final TodoMapper todoMapper;
    private final ModelMapper modelMapper;

    @Override
    public void register(TodoDTO todoDTO) {
        // 1) todoDTO를 전달 받아 2) todoDTO를 todoVO로 변환 후 3) dao의 insert()호출
        log.info(todoDTO);
        TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);
        log.info(todoVO);
        todoMapper.insert(todoVO);
    }
}

 

service 패키지는 root-context.xml에서 component-scan 패키지로 추가

<context:component-scan base-package="com.example.spring_ex_01_2404.service"/>

 


3)  TodoService 테스트


서비스 계층에서 DTO를 VO로 변환하는 작업을 처리하기 때문에 가능하면 테스트를 진행해서 문제가 없는지 확인하는 것이 좋음
test 폴더내에 service 관련 패키지를 생성하고 TodoServiceTest 클래스를 작성

@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
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);
    }
}


4)  TodoController의 GET / POST 처리


서비스 계층까지 문제 없이 동작하는 것을 확인했다면 스프링 MVC를 처리
입력할 수 있는 화면을 위해 controller 패키지의 TodoController를 확인

 

TodoController에 GET 방식으로 '/todo/register'가 가능한지 확인
@Log4j2
@Controller
@RequestMapping("/todo")
@RequiredArgsConstructor
public class TodoController {

    @GetMapping("/register")
    public void register() {
        log.info("todo register...");
    }
}

 

/WEB-INF/view/todo/ 폴더에 register.jsp는 test.html을 복사해서 구성. 상단에 JSP 관련 설정을 추가.
register.jsp에 class 속성이 "card-body"로 지정된 부분의 코드를 수정
입력하는 화면의 디자인은 https://getbootstrap.com/docs/5.1/forms/form-control/

 

Form controls

Give textual form controls like <input>s and <textarea>s an upgrade with custom styles, sizing, focus states, and more.

getbootstrap.com

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<div class="card-body">
    <form  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">
        </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="reset" class="btn btn-secondary">Reset</button>
            </div>
        </div>
    </form>
</div>

 

 POST 방식의 처리


    1.  register.jsp의 <form method="post"> 태그에 의해서 submit 버튼을 클릭하면 POST 방식으로 "title, dueDate, writer"를 전송하게 됨
    2.  TodoController에서는 TodoDTO로 바로 전달된 파라미터의 값들을 수집
    3.  POST 방식으로 처리된 후에는 "/register/list"로 이동해야 하므로 "redirect:/todo/list"로 이동할 수 있도록 문자열을 반환

@PostMapping("/register")
public String registerPOST(TodoDTO todoDTO) {
    log.info("POST todo register");
    log.info(todoDTO);
    return "redirect:/todo/list";
}

 

  ✓  한글 문제가 있기는 하지만 브라우저에서 입력한 데이터들이 수집되고 /todo/list로 이동하는 기능에 문제가 없는 것을 확인

 


2.  한글 처리를 위한 필터 설정

서버의 한글 처리에 대한 설정은 스프링 MVC에서 제공하는 필터로 쉽게 처리할 수 있음

 

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>

 

  ✓  web.xml의 설정을 서버를 재시작해야 올바르게 반영되므로 톰캣을 재시작하고 한글 처리를 확인


3.  @Valid를 이용한 서버사이드 유효성 검증

유효성 검증


과거의 웹 개발에는 자바 스크립트를 이용하여 브라우저에서만 유효성 검사를 진행하는 방식이 많았지만, 모바일과 같이 다양한 환경에서 서버를 이용하는 현재에는 브라우저를 사용하는 프론트쪽에서의 검증과 더불어 서버에서도 입력되는 값들을 검증하는 것이 좋음. 이러한 검증 작업은 컨트롤러에서 진행하는데 스프링MVC의 경우 @Valid와 BindingResult라는 존재를 이용해서 간단하게 처리할 수 있음


  ⚡️  스프링 MVC에서 검증을 처리하기 위해서는 hibernate-validate 라이브러리가 필요 (build.gradle 에 추가)

// https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator
implementation 'org.hibernate.validator:hibernate-validator:6.2.1.Final'

 

hibernate-validate를 이용해서 사용하는 대표적인 어노테이션
어노테이션 설명 어노테이션 설명
@NotNull Null 불가 @Null Null만 입력 가능
@NotEmpty Null 빈 문자열 불가 @NotBlank Null 빈 문자열, 스페이스만 있는 문자열 불가
@SIze(min=, max=) 문자열, 배열 등의 크기가
만족하는가
@Pattern(regex=) 정규식을 만족하는가
@Max(num) 지정 값 이하인가 @Min(num) 지정 값 이상인가
@Future 현재 보다 미래인가 @Past 현재 보다 과거인가
@Positive 양수만 가능 @PositiveOrZero 양수와 0만 가능
@Negative  음수만 가능 @NegativeOrZero 음수와 0만 가능

 

 

1) TodoDTO 검증하기

 

TodoDTO에 간단한 어노테이션 적용
@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;
}

 

 

TodoController에서 POST 방식으로 처리할 때 이를 반영하도록 BindingResult와 @Valid 어노테이션을 적용
 @PostMapping("/register")
    public String registerPOST(@Valid TodoDTO todoDTO, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
        log.info("POST todo register");

        if (bindingResult.hasErrors()) {
            log.info("has error...");
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            return "redirect:/todo/register";
        }

        log.info(todoDTO);
        return "redirect:/todo/list";
    }

 

  ✓  TodoDTO 에는 @Valid를 적용하고, BindingResult 타입을 파라미터로 새롭게 추가

    ➡️  BindingResult는  스프링이 제공하는 검증 오류를 보관하는 객체

  ✓  registerPOST() 에서는 hasErrors()을 이용해서 검증에 문제가 있다면 다시 입력화면으로 리다이렉트되도록 처리


  ✓  처리 과정에서 잘못된 결과는 RedirectAttributes의 addFlashAttribute()를 이용해서 전달

    ➡️ flash 속성에 객체를 저장, 요청 매개 변수(requestparameters)로 값을 전달하지않고 객체로 값을 그대로 전달. 일회성으로 한번 사용하면 Redirect후 값이 소멸


  ✓  TodoDTO의 writer는 @NotEmpty가 적용되어 있으므로 항목을 입력하지 않고 submit을 하면 다시 입력화면으로 돌아감

 

 

 

 

 


 

2) JSP에서 검증 에러 메시지 확인하기

 

register.jsp에 검증된 결과를 확인하기 위해 상단에 태그 라이브러리 추가
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

 

자바스크립트 객체를 생성해 둔 후 필요할 때 화면에서 처리
<form>태그가 끝난 후에 <script> 태그를 추가
<script>
    const serverValidResult = {};
    <c:forEach items="${errors}" var="error">
    serverValidResult['${error.getField()}'] = '${error.defaultMessage}';
    </c:forEach>
    console.log(serverValidResult);
</script>

 

  ✓  자바스크립트를 이용해서 오류 객체를 생성하면 나중에 화면에서 자유롭게 처리할 수 있다는 장점이 있음

  ✓  아무것도 입력하지 않은 상태에서 submit을 하면 다음과 같은 코드가 출력

아무 것도 입력하지 않았을 때와 과거 날짜를 입력했을 때 콘솔 창 출력 결과

 


4.  Todo 등록 기능 완성

입력값의 검증까지 끝났다면 최종적으로 TodoService를 주입하고, 연동하도록 구성

 

TodoController의 클래스 선언부에서 TodoService를 주입
@Log4j2
@Controller
@RequestMapping("/todo")
@RequiredArgsConstructor
public class TodoController {
    private final TodoService todoService;

 

registerPOST()에서는 TodoService의 기능을 호출하도록 구성
 @PostMapping("/register")
    public String registerPOST(@Valid TodoDTO todoDTO, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
        log.info("POST todo register");

        if (bindingResult.hasErrors()) {
            log.info("has error...");
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            return "redirect:/todo/register";
        }

        log.info(todoDTO);
        todoService.register(todoDTO); // 호출 코드 생성
        return "redirect:/todo/list";
    }

 

 

 

 

모든 기능의 개발이 완료 되었다면 등록 후에 "/todo/list"로 이동하게 됨. 아직 "/todo/list"의 개발은 완료되지 않았으니 데이터베이스를 이용해서 최종 확인

 

 

 

 

 

 

 

 

 

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1. 테이블 레이아웃

테이블레이아웃은 위젯을 표 형태로 배치할 때 주로 활용. 테이블레이아웃을 사용하여 행과 열의 수를 정하고 그 안에 위젯을 배치. <TableRow>가 행의 수, 열의 수는 <TableRow> 안에 포함된 위젯의 수로 결정

3행 4열의 테이블레이아웃

 


1) 테이블레이아웃의 속성

 

  • layout_span과 layout_column은 테이블레이아웃 안에 포함된 위젯에 설정하는 속성
    - layout_span은 열을 합쳐서 표시하라는 의미로, 예를 들어 layout_span="2"는 현재 셀부터 2개의 셀을 합쳐서 표시
    - layout_column은 지정된 열에 현재 위젯을 표시하라는 의미
  • stretchColumns은 <TableLayout> 자체에 설정하는 속성으로, 지정된 열의 너비를 늘이라는 의미
    - stretchColumns="*"는 각 셀을 모두 같은 크기로 확장하여 전체 화면을 꽉 차는 효과를 냄. 열번호는 0부터 시작.
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TableRow>
        <Button
            android:layout_width="60dp"
            android:text="1" />

        <Button
            android:layout_width="60dp"
            android:layout_span="2"
            android:text="2" />

        <Button
            android:layout_width="60dp"
            android:text="3" />
    </TableRow>

    <TableRow>
        <Button
            android:layout_width="60dp"
            android:layout_column="1"
            android:text="4" />

        <Button
            android:layout_width="60dp"
            android:text="5" />

        <Button
            android:layout_width="60dp"
            android:text="6" />
    </TableRow>

</TableLayout>


2.  그리드 레이아웃

테이블레이아웃과 마찬가지로 위젯을 표 형태로 배치할 때 사용하지만 좀 더 직관적
테이블레이아웃에서는 다소 어려웠던 행 확장도 간단하게 할 수 있어서 유연한 화면 구성에 적합.
  ✓  행과 열을 지정하는 방법 :  2행 3열을 지정하려면 layout_row 속성은 1로, layout_column 속성은 2로 설정

 

1)  그리드레이아웃의 속성

  • rowCount : 행의 수
  • columnCount : 열의 수
  • orientation : 그리드를 수평 방향으로 우선할 것인지, 수직 방향으로 우선할 것인지를 결정
     -  그리드레이아웃 안에 포함될 위젯에서 자주 사용되는 속성
  • layout_row : 자신이 위치할 행 번호(0번부터 시작)
  • layout_column : 자신이 위치할 열 번호(0번부터 시작)
  • layout_rowSpan : 행을 지정된 수만큼 확장
  • layout_columnSpan : 열을 지정된 수만큼 확장
  • layout_gravity : 주로 fill, fill_vertical, fill_horizontal 등으로 지정

    💡  layout_rowSpan이나 layout_columnSpan으로 셀 확장 시 위젯을 확장된 셀에 꽉 채우는 효과를 줌
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:rowCount="2"
    android:columnCount="4">

    <Button
        android:layout_gravity="fill_vertical"
        android:layout_column="0"
        android:layout_row="0"
        android:layout_rowSpan="2"
        android:text="1" />

    <Button
        android:layout_gravity="fill_horizontal"
        android:layout_column="1"
        android:layout_columnSpan="2"
        android:layout_row="0"
        android:text="2" />

    <Button
        android:layout_column="3"
        android:layout_row="0"
        android:text="3" />

    <Button
        android:layout_column="1"
        android:layout_row="1"
        android:text="4" />

    <Button
        android:layout_column="2"
        android:layout_row="1"
        android:text="5" />

    <Button
        android:layout_column="3"
        android:layout_row="1"
        android:text="6" />

</GridLayout>


3.  프레임 레이아웃

 

 

단순히 레이아웃 안의 위젯을 왼쪽 상단부터 겹쳐서 출력
프레임레이아웃 그 자체를 사용하기 보다는 탭 위젯 등과 혼용할 때 유용

 

👩🏻‍💻  프레임레이아웃의 속성

  •  foreground : 프레임레이아웃의 전경 이미지를 지정
  •  foregroundGravity : 전경 이미지의 위치를 지정. fill, right, left, top, bottom 등의 값을 사용

 

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:foreground="@drawable/dog2"
    android:foregroundGravity="center|fill_horizontal">

    <RatingBar
        android:id="@+id/ratingBar1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="80dp"/>

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />

    <CheckBox
        android:id="@+id/checkBox1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="CheckBox" />

</FrameLayout>

 

 

 

[ 내용 참고 : IT 학원 강의 ]

+ Recent posts