요구 사항
- 목록 처리
- 등록 처리
- 조회 처리
- 수정 처리
- 삭제 처리
CRUD 컨트롤러와 화면 개발
(1) 컨트롤러 작성
(2) 화면 작성
(3) 화면에서 테스트
목록 처리
1. 컨트롤러 작성
-BoardController에 list() 메서드 작성
package org.zerock.b01.controller;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.zerock.b01.dto.BoardDTO;
import org.zerock.b01.dto.PageRequestDTO;
import org.zerock.b01.dto.PageResponseDTO;
import org.zerock.b01.service.BoardService;
@Controller
@RequestMapping("/board")
@Log4j2
@RequiredArgsConstructor
public class BoardController {
private final BoardService boardService;
@GetMapping("/list")
public void list(PageRequestDTO pageRequestDTO, Model model){
PageResponseDTO<BoardDTO> responseDTO = boardService.list(pageRequestDTO);
log.info(responseDTO);
model.addAttribute("responseDTO", responseDTO);
}
}
-> 파라미터로 PageRequestDTO와 PageResponseDTO가 담긴 model을 전달
-> 즉, 화면으로 PageRequestDTO와 PageResponseDTO가 전달
2. 화면 작성
- 부트스트랩의 card를 감싸는 여러개의 <div> 추가해, <div>를 이용하는 방식으로 화면 구성
https://getbootstrap.kr/docs/5.1/components/card/
사용할 카드 기본 문법
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<h5 class="card-title">Special title treatment</h5>
<p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
(1) list.html 작성
- div:row mt-3
- div:col
- div:card
- div:card-body
- h5:card-title
- table
- thead
- tr
- th : col
- tr
- tbody
- tr
- th : row
- td
- tr
- thead
- table
- div:card
- div:col
<!-- 타임리프 레이아웃을 적용해 본문만 적용-->
<div layout:fragment="content">
<!-- <h1> Board List</h1>-->
<div class="row mt-3">
<div class="col">
<div class="card">
<div class="card-header">
Board List-header
</div>
<div class="card-body">
<h5 class="card-title">
Board List-title
</h5>
<table class="table">
<thead>
<tr>
<th scope="col">Bno</th>
<th scope="col">Title</th>
<th scope="col">Writer</th>
<th scope="col">RegDate</th>
</tr>
</thead>
<tbody>
<tr th:each="dto:${responseDTO.dtoList}">
<th scope="row">
[[${dto.bno}]]
</th>
<td>
[[${dto.title}]]
</td>
<td>
[[${dto.writer}]]
</td>
<td>
[[${dto.regDate}]]
</td>
</tr>
</tbody>
</table>
</div><!--end card body-->
</div><!--end card-->
</div><!-- end col-->
</div><!-- end row-->
</div><!-- end content-->
(2) regDate 날짜 포맷팅 처리
-Thymeleaf의 #temporals.format 유틸리티 객체를 이용해 처리
변경 전, 기본 날짜 포맷의 등록일
<tbody>
<tr th:each="dto:${responseDTO.dtoList}">
<th scope="row">
[[${dto.bno}]]
</th>
<td>
[[${dto.title}]]
</td>
<td>
[[${dto.writer}]]
</td>
<td>
[[${dto.regDate}]]
</td>
</tr>
</tbody>
변경 후, 'yyyy-MM-dd' 날짜 포맷의 등록일
<tbody>
<tr th:each="dto:${responseDTO.dtoList}">
<th scope="row">
[[${dto.bno}]]
</th>
<td>
[[${dto.title}]]
</td>
<td>
[[${dto.writer}]]
</td>
<td>
[[${#temporals.format(dto.regDate, 'yyyy-MM-dd')}]]
</td>
</tr>
</tbody>
(3) 페이지네이션 컴포넌트를 이용해 페이지 목록 출력
-테이블의 마지막에 <div>태그를 이용해 페이지 번호 출력
-#numbers.sequence : PageResponseDTO 객체는 시작 번호, 끝 번호만 가지고 있으므로, 특정 범위의 연속된 숫자를 만드는 유틸리티 객체
-'data-num' : #number 유틸리티 객체로 만든 숫자를 페이지 번호로 사용하기 위한 속성
-flex : 줄바꿈 속성
- .flex-nowrap : 줄바꿈을 없애거나 (브라우저 기본값)
- .flex-wrap : 줄바꿈
- .flex-wrap-reverse : 역방향으로 줄바꿈
https://getbootstrap.kr/docs/5.1/components/pagination/
https://getbootstrap.kr/docs/5.1/utilities/flex/
aria-label을 사용하여 요소에 액세스 가능한 이름을 명시적으로 설정
https://getbootstrap.kr/docs/5.2/forms/overview/#%EC%A0%91%EA%B7%BC%EC%84%B1
페이지 번호 출력 기본 문법
<nav aria-label="Page navigation example">
<ul class="pagination">
<li class="page-item"><a class="page-link" href="#">Previous</a></li>
<li class="page-item"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item"><a class="page-link" href="#">Next</a></li>
</ul>
</nav>
<!-- #numbers 유틸리티 객체를 이용해 페이지 번호와 앞페이지, 뒤페이지 버튼 만들기-->
<div class="float-end">
<ul class="pagination flex-wrap">
<!-- 전달받은 responseDTO 객체의 prev 변수가 존재하면 노출-->
<li class="page-item" th:if="${responseDTO.prev}">
<a class="page-link" th:data-num="${responseDTO.start -1}">
Previous
</a>
</li>
<!-- 특정 범위의 연속된 번호를 이용해 페이지 번호 출력-->
<!-- 현재 페이지 나타내기 -->
<!-- th:block, th:each, th:class, th:data-num, #numbers.sequence 이용-->
<th:block th:each="i: ${#numbers.sequence(responseDTO.start, responseDTO.end)}">
<!--삼항 연산자를 이용해 현재 페이지는 active 속성 적용 -->
<li th:class="${responseDTO.page==i}?'page-item active':'page-item'">
<a class="page-link" th:data-num="${i}">[[${i}]]</a>
</li>
</th:block>
<!--전달받은 responseDTO 객체의 next 변수가 존재하면 노출-->
<li class="page-item" th:if="${responseDTO.next}">
<a class="page-link" th:data-num="${responseDTO.end+1}">
Next
</a>
</li>
</ul>
</div>
-> 직접 URL 변경하는 방식으로 페이지 번호를 쿼리 스트링으로 추가해 페이지가 변경된다.
(4) 검색 화면 추가
- <table> 태그 위에 카드 컴포넌트를 이용해 검색 화면을 구현
-검색 조건이 페이지 이동과 함께 처리 되도록 <form> 태그로 감싼다.
-'input-group' : 같은 행
-'input-group-prepend' : 같은 행에 존재하지만, 따로 떨어뜨리고 싶을 때
#부트스트랩최신 버전에서
input-group-append와 .input-group-prepend는 삭제.
입력 그룹의 직접 자식 요소로서 버튼과 .input-group-text를 추가할 수 있게 되었습니다.
https://getbootstrap.kr/docs/5.2/forms/input-group/
input-group-append 이용
<!-- 검색 화면 - 1열-->
<div class="row mt-3">
<form action="/board/list" method="get">
<div class="col">
<input type="hidden" name="size" th:value="${pageRequestDTO.size}">
<div class="input-group">
<div class="input-group-append">
<!--검색 조건 -->
<select class="form-select" name="type">
<option value="">---</option>
<option value="t" th:selected="${pageRequestDTO.type=='t'}">제목</option>
<option value="c" th:selected="${pageRequestDTO.type=='c'}">내용</option>
<option value="w" th:selected="${pageRequestDTO.type=='w'}">작성자</option>
<option value="tc" th:selected="${pageRequestDTO.type=='tc'}">제목 내용</option>
<option value="tcw" th:selected="${pageRequestDTO.type=='tcw'}">제목 내용 작성자</option>
</select>
</div>
<input type="text" class="form-control" name="keyword" th:value="${pageRequestDTO.keyword}">
</span>
<div class="input-group-append">
<button class="btn btn-outline-secondary searchBtn" type="submit">Search</button>
<button class="btn btn-outline-secondary clearBtn" type="submit">Clear</button>
</div>
</div>
</div>
</form>
</div>
input-group-addon 이용
<div class="input-group">
<span class="input-group-addon">
<!--검색 조건 -->
<select class="form-select" name="type">
<option value="">---</option>
<option value="t" th:selected="${pageRequestDTO.type=='t'}">제목</option>
<option value="c" th:selected="${pageRequestDTO.type=='c'}">내용</option>
<option value="w" th:selected="${pageRequestDTO.type=='w'}">작성자</option>
<option value="tc" th:selected="${pageRequestDTO.type=='tc'}">제목 내용</option>
<option value="tcw" th:selected="${pageRequestDTO.type=='tcw'}">제목 내용 작성자</option>
</select>
</span>
<input type="text" class="form-control" name="keyword" th:value="${pageRequestDTO.keyword}">
<span class="input-group-addon">
<button class="btn btn-outline-secondary searchBtn" type="submit" id="button-addon1">Search</button>
<button class="btn btn-outline-secondary clearBtn" type="submit">Clear</button>
</span>
</div>
(5) 이벤트 처리 (자바스크립트)
요구사항
- 페이지 번호 클릭
- 검색 창에 있는 <form> 태그에 <input type='hidden'>으로 page 추가 후 submit
- 검색/필터링 조건
- Clear 버튼 클릭시 검색 조건 없이 '/board/list' 호출
- JSP의 자바스크립트 문자열 처리 : ₩${~}
- 타임리프의 자바스크립트 문자열 처리 : '${~}'
자바스크립트 메서드
이벤트가 발생했을 때 실행되는 함수인 핸들러(handler)를 할당
- HTML만 사용하는, HTML 속성에 핸들러 할당
- HTML과 자바스크립트를 함께 사용하는, DOM 프로퍼티에 핸들러 할당
HTML 속성에 핸들러 할당
<input type="button" onclick="alert('클릭!')" value="클릭해 주세요.">
DOM 프로퍼티에 핸들러 할당
<input type="button" id="button" value="클릭해 주세요.">
<script>
button.onclick = function() {
alert('클릭!');
};
</script>
이벤트를 추가하는 메서드
하나의 이벤트에 복수의 핸들러를 사용하지 못하는 문제를 해결하기 위한 메서드
- addEventListener : 핸들러 추가
- removeEventListener : 핸들러 삭제
https://ko.javascript.info/introduction-browser-events
요소 검색하는 메서드
- document.getElementById(id) : id 속성을 이용해 접근
- elem.querySelector(css) : 주어진 CSS 선택자에 대응하는 요소 중 첫 번째 요소를 반환
https://ko.javascript.info/searching-elements-dom
이벤트 처리 메서드 - 예외 처리 발생시 사용하는 메서드
- e.preventDefault() :html 에서 a 태그나 submit 태그는 고유의 동작이 있다. 페이지를 이동시킨다거나 form 안에 있는 input 등 을 전송한다던가 그러한 동작이 있는데 e.preventDefault 는 그 동작을 중단시킨다.
[브라우저 기본동작 : https://ko.javascript.info/default-browser-action ] - e.stopPropagation() : e.stopPropagation 는 상위 엘리먼트들로의 이벤트 전파를 중단시킨다.
[버블링과 캡처링 : https://ko.javascript.info/bubbling-and-capturing ]
속성에 접근하는 메서드
- elem.hasAttribute(name) – 속성 존재 여부 확인
- elem.getAttribute(name) – 속성값을 가져옴
- elem.setAttribute(name, value) – 속성값을 변경함
- elem.removeAttribute(name) – 속성값을 지움
https://ko.javascript.info/dom-attributes-and-properties
HTML을 문자열 형태로 받아오는 프로퍼티 - innerHTML
- alert( document.body.innerHTML ); //읽기
- document.body.innerHTML = '새로운 BODY!'; // 교체
- elem.innerHTML+="추가 html"; //추가
//특수문자를 문자열에 추가하기 위해서, '\'를 이용해야할 수도 있다. - elem.innerHTML+=`문자열 결합+'${~}'`;
//백틱을 이용해, 문자열 결합에 '+'를 이용하지 않고, 문자열 결합을 수행한다.
//단, JSP에서는 '\${~}'로 표현해야한다.
https://ko.javascript.info/basic-dom-node-properties
https://ko.javascript.info/string
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")
const formObj = document.querySelector("form")
formObj.innerHTML += `<input type='hidden' name='page' value='${num}'>`
formObj.submit();
},false)
document.querySelector(".clearBtn").addEventListener("click", function (e){
e.preventDefault()
e.stopPropagation()
self.location ='/board/list'
},false)
등록 처리
@Valid를 이용해 서버에서 검증한 후에 등록하는 방식 적용
(1) 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-validation:2.7.3'
(2) BoardDTO에 검증하기 위한 어노테이션 추가
javax.validation.constraints 어노테이션
- @NotNull, @Null, @NotEmpty, @NotBlank
- @Size(min=, max=), @Pattern(regex=)
- @Max(num), Min(num)
- @Future @Past
- @Positive @PositiveOrZero @Negative @NegativeOrZero
package org.zerock.b01.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.zerock.b01.domain.QBaseEntity;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;
import java.time.LocalTime;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BoardDTO {
private Long bno;
@NotEmpty
@Size(min=3, max=100)
private String title;
@NotEmpty
private String content;
@NotEmpty
private String writer;
private LocalDateTime regDate;
private LocalDateTime modDate;
}
1. 컨트롤러 작성
-GET 방식으로 화면 전달하는 registerGet() 메서드 작성, POST 방식으로 입력 폼 전달하는 registerPost() 메서드 작성
-BoardDTO를 받아 @Valid을 이용해 검증하고,
검증에 걸리면 BindingResult으로 'error'라는 이름으로 RedirectAttributes에 담아서 전송.
검증에 안걸리면, 'result'라는 이름으로 RedirectAttributes에 담아서 전송
-BindingResult 타입을 파라미터로 추가
-hasErrors() / getAllErrors() : 예외처리 메서드로, 에러가 발견되면 입력 화면으로 리다이렉트하며 addFlashAttribute()로 해당 데이터 전송
RedirectAttributes와 리다이렉션
PRG패턴을 처리하기 위해서 스프링 MVC에서 지원하는 RedirectAttributes 타입
- addAttribute(키, 값) : 리다이렉트할 때 쿼리 스트링이 되는 값을 지정
- addFlashAttribute(키, 값) : 일회용으로만 데이터를 전달하고 삭제되는 값을 지정
@GetMapping("/register")
public void registerGet(){
}
@PostMapping("/register")
public String registerPost(@Valid BoardDTO boardDTO, BindingResult bindingResult, RedirectAttributes redirectAttributes){
log.info("board POST register...");
if(bindingResult.hasErrors()){
log.info("hasErrors");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
return "redirect:/board/register";
}
log.info(boardDTO);
Long bno=boardService.register(boardDTO);
redirectAttributes.addFlashAttribute("result", bno);
return "redirect:/board/list";
}
2. 화면 작성
(1) templates/board/register.html 작성
-타임리프, 레이아웃 네임스페이스 추가
-레이아웃으로 사용할 html 지정
-기본 카드 컴포넌트 사용
<div class="card">
<h5 class="card-header">Featured</h5>
<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>
https://getbootstrap.kr/docs/5.2/components/card/
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:th="http://www.thymeleaf.org"
layout:decorate="~{layout/basic.html}">
<head>
<meta charset="UTF-8">
<title>Board Register-title</title>
</head>
<div layout:fragment="content">
<div class="row mt-3">
<div class="col">
<div class="card">
<div class="card-header">
Board Register-header
</div>
<div class="card-body">
</div>
</div>
</div>
</div>
</div>
<script layout:fragment="script" th:inline="javascript">
</script>
(2) <form> 태그를 이용해 게시물에 입력 항목들을 추가한다.
<form action="/board/register" method="post">
<!--제목 -->
<div class="input-group mb-3">
<div class="input-group-text">Title</div>
<input type="text" name="title" class="form-control" placeholder="Title">
</div>
<!--내용 -->
<div class="input-group mb-3">
<div class="input-group-text">Content</div>
<textarea class="form-control col-sm-5" rows="5" name="content"></textarea>
</div>
<!--작성자-->
<div class="input-group mb-3">
<div class="input-group-text">Writer</div>
<input type="text" name="writer" class="form-control" placeholder="Writer">
</div>
</form>
(3) <form> 태그 안에 등록과 초기화 버튼 추가
-float 유틸리티 : 반응형 요소로 반응형으로 움직이는 위치를 지정할 수 있다.
https://getbootstrap.kr/docs/5.2/utilities/float/#%EB%B0%98%EC%9D%91%ED%98%95
<!-- 카드 본문에 <form>태그로 게시물에 입력할 항목 추가-->
<div class="card-body">
<form action="/board/register" method="post">
<!--제목 -->
<div class="input-group mb-3">
<div class="input-group-text">Title</div>
<input type="text" name="title" class="form-control" placeholder="Title">
</div>
<!--내용 -->
<div class="input-group mb-3">
<div class="input-group-text">Content</div>
<textarea class="form-control col-sm-5" rows="5" name="content"></textarea>
</div>
<!--작성자-->
<div class="input-group mb-3">
<div class="input-group-text">Writer</div>
<input type="text" name="writer" class="form-control" placeholder="Writer">
</div>
<!--등록 / 초기화 버튼-->
<div class="float-end">
<button type="submit" class="btn btn-primary">Submit</button>
<button type="reset" class="btn btn-primary">Reset</button>
</div>
</form>
(4) @Valid의 에러 메시지 처리 (자바스크립트)
-@Valid를 통해 검증 후, 검증 실패시 앞의 화면으로 이동
-'error' : addFlashAttributes()로 에러 메시지를 전달 받는다.
-alert() : 알림창 표시
-백틱을 이용해 문자열 결합 : `( ${~} )`
<script layout:fragment="script" th:inline="javascript">
const errors=[[${error}]]
console.log(errors)
let errorMsg=''
//null이 아니라면, 즉 존재한다면
if(errors){
for(let i=0;i<errors.length;i++){
errorMsg+=`${errors[i].field}은(는) ${errors[i].code} \n`
}
alert(errorMsg)
}
</script>
(5) 모달창 컴포넌트를 이용해 이벤트 처리 (자바스크립트)
https://getbootstrap.kr/docs/5.2/components/modal/
-정상 등록 시, '/board/list'로 이동
-'result' : 컨트롤러에서 RedirectAttributes의 addFlashAttributes()로 전달한 데이터로 쿼리스트링으로 처리되지 않고 브라우저 내부에 일회성으로 저장된다.
-해당 일회성 메시지를 이용해 모달창 처리
//'result' 모달창 처리
const result=[[${result}]]
if(result){
alert(result)
}
기본 모달창 구조
<div class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Modal body text goes here.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
-템플릿 안에 존재하기 위해서,list.html의 본문 마지막에 추가
<div class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Modal body text goes here.</p>
</div>
<!--취소 / 저장 버튼-->
<!--정상 작성 완료를 나타내는 모달창으로, 저장버튼은 작동하지 않는 상태-->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
-모달창을 띄우는 자바스크립트 작성
//'result' 모달창 처리
const result=[[${result}]]
//모달창 표시
const modal=new bootstrap.Modal(document.querySelector(".modal"))
if(result){
alert(result)
modal.show()
}
조회 처리
특정 번호의 게시물을 조회
1. 컨트롤러 작성
-BoardController에 read() 메서드 작성
-조회 후, 목록으로 돌아올 때 검색 조건을 유지하도록 PageRequestDTO를 함께 전달한다.
@GetMapping("/read")
public void read(Long bno, PageRequestDTO pageRequestDTO, Model model){
BoardDTO boardDTO=boardService.readOne(bno);
log.info(boardDTO);
model.addAttribute("dto", boardDTO);
}
-> GetMapping의 경우 modify와 같은 매핑을 수행한다.
2. 화면 작성
(1) read.html 작성
-register.html와 유사하지만 항목들이 모두 읽기전용이다.
- Modify, Remove 버튼
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:th="http://www.thymeleaf.org"
layout:decorate="~{layout/basic.html}">
<head>
<meta charset="UTF-8">
<title>Board Read-title</title>
</head>
<div layout:fragment="content">
<div class="row mt-3">
<div class="col">
<div class="card">
<!--굵음-->
<div class="card-header">
Board Read-header
</div>
<!-- 카드 본문에 <form>태그로 게시물에 입력할 항목 추가-->
<div class="card-body">
</div>
</div>
</div>
</div>
</div>
<script layout:fragment="script" th:inline="javascript">
</script>
(2) 컨트롤러에서 'dto'로 전달된 Model을 출력
-'card-body' 부분에 출력하는데, 읽기전용으로 설정
-수정, 삭제 버튼 만들기
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:th="http://www.thymeleaf.org"
layout:decorate="~{layout/basic.html}">
<head>
<meta charset="UTF-8">
<title>Board Read-title</title>
</head>
<div layout:fragment="content">
<div class="row mt-3">
<div class="col">
<div class="card">
<!--굵음-->
<div class="card-header">
Board Read-header
</div>
<!-- 카드 본문에 <form>태그로 게시물에 입력할 항목 추가-->
<div class="card-body">
<!-- 글번호-->
<div class="input-group mb-3">
<div class="input-group-text">Bno</div>
<input type="text" class="form-control" th:value="${dto.Bno}" readonly>
</div>
<!--제목 -->
<div class="input-group mb-3">
<div class="input-group-text">Title</div>
<input type="text" class="form-control" th:value="${dto.title}" readonly>
</div>
<!--내용 -->
<div class="input-group mb-3">
<div class="input-group-text">Content</div>
<textarea class="form-control col-sm-5" rows="5" readonly>[[${dto.title}]]</textarea>
</div>
<!--작성자-->
<div class="input-group mb-3">
<div class="input-group-text">Writer</div>
<input type="text" class="form-control" th:value="${dto.writer}" readonly>
</div>
<!--등록시간-->
<div class="input-group mb-3">
<div class="input-group-text">Writer</div>
<input type="text" class="form-control" th:value="${#temporals.format(dto.regDate, 'yyyy-MM-dd HH:mm:ss')}" readonly>
</div>
<!--수정시간-->
<div class="input-group mb-3">
<div class="input-group-text">Writer</div>
<input type="text" class="form-control" th:value="${#temporals.format(dto.modDate, 'yyyy-MM-dd HH:mm:ss')}" readonly>
</div>
<!--등록 / 초기화 버튼-->
<div class="my-4">
<div class="float-end">
<button type="button" class="btn btn-primary">List</button>
<button type="button" class="btn btn-secondary">Modify</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script layout:fragment="script" th:inline="javascript">
</script>
(3) read.html의 'List' 버튼 이동 처리
-'List' 버튼 : 목록 페이지로 이동하는데, 검색 조건을 그대로 유지한채로 이동한다.
-th:with : 필요할 때마다 재사용하기 위해 변수 설정
-th:href : 각 버튼을 감싸는 <a> 태그를 이용해 PageRequestDTO의 getLink()를 활용해 링크 처리
변경 전, 버튼만 존재
<!--목록 / 수정 버튼-->
<div class="my-4">
<div class="float-end">
<button type="button" class="btn btn-primary">List</button>
<button type="button" class="btn btn-secondary">Modify</button>
</div>
</div>
변경 후, 버튼클릭시 검색 조건을 그대로 유지한 채로 목록으로 이동
<!--목록 / 수정 버튼-->
<div class="my-4">
<div class="float-end" th:with="link = ${PageRequestDTO.getLink()}">
<a th:href="|@{/board/list}?${link}|" class="text-decoration-none">
<button type="button" class="btn btn-primary">List</button>
</a>
<a th:href="|@{/board/modify}?${dto.bno}&${link}|" class="text-decoration-none">
<button type="button" class="btn btn-secondary">Modify</button>
</a>
</div>
</div>
텍스트 꾸미기
https://getbootstrap.kr/docs/5.2/utilities/text/#text-decoration
<!--목록 / 수정 버튼-->
<div class="my-4">
<div class="float-end" th:with="link = ${pageRequestDTO.getLink()}">
<a th:href="|@{/board/list}?${link}|" class="text-decoration-none">
<button type="button" class="btn btn-primary">List</button>
</a>
<a th:href="|@{/board/modify(bno=${dto.bno})}&${link}|" class="text-decoration-none">
<button type="button" class="btn btn-secondary">Modify</button>
</a>
</div>
</div>
(4) 목록에서 게시물 링크 처리
-목록을 반복문 처리할 때 PageRequestDTO의 getLink() 결과를 th:with을 이용해 재사용가능한 변수로 처리해 링크 완성
-검색 조건 유지한 채로 이동
변경 전, 링크가 없는 게시물 리스트
<tbody>
<tr th:each="dto:${responseDTO.dtoList}">
<th scope="row">
[[${dto.bno}]]
</th>
<td>
[[${dto.title}]]
</td>
<td>
[[${dto.writer}]]
</td>
<td>
<!--#temporals 유틸리티 객체를 이용해 날짜 포맷팅 -->
[[${#temporals.format(dto.regDate, 'yyyy-MM-dd')}]]
</td>
</tr>
</tbody>
변경 후, 링크 처리된 게시물 리스트
<!--th:with을 이용해 재사용해서 게시물의 링크처리 -->
<tbody th:with="link =${pageRequestDTO.getLink()}">
<tr th:each="dto:${responseDTO.dtoList}">
<th scope="row">
[[${dto.bno}]]
</th>
<td>
<a th:href="|@{/board/read(bno=${dto.bno})}&${link}|">
[[${dto.title}]]
</a>
</td>
<td>
[[${dto.writer}]]
</td>
<td>
<!--#temporals 유틸리티 객체를 이용해 날짜 포맷팅 -->
[[${#temporals.format(dto.regDate, 'yyyy-MM-dd')}]]
</td>
</tr>
</tbody>
수정/삭제 처리
수정/삭제 처리는 GET 방식으로 미리 작성된 read() 메서드를 재사용한다.
(1) read() 메서드의 URLpatterns에 수정 페이지 추가
@GetMapping({"/read", "/modify"})
public void read(Long bno, PageRequestDTO pageRequestDTO, Model model){
BoardDTO boardDTO=boardService.readOne(bno);
log.info(boardDTO);
model.addAttribute("dto", boardDTO);
}
(2) 수정/삭제 화면을 담당하는 modify.html 작성
수정 처리
1. 컨트롤러에 POST 방식의 modify() 작성
-addFlashAttribute : 일회성 데이터로 에러를 저장한다.
-addAttribute : 데이터를 저장해 화면에 전달한다.
@PostMapping("/modify")
public String modify(PageRequestDTO pageRequestDTO, @Valid BoardDTO boardDTO, BindingResult bindingResult, RedirectAttributes redirectAttributes){
log.info("board modify post..."+boardDTO);
//검증 실패시 - 에러 메시지 전송 및 이전 페이지로 이동
if(bindingResult.hasErrors()){
log.info("modify post hasErrors");
String link= pageRequestDTO.getLink();
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
redirectAttributes.addAttribute("bno", boardDTO.getBno());
return "redirect:/board/modify?"+link;
}
//검증 성공시 - 수정 수행
boardService.modify(boardDTO);
//수정 완료 메시지 전송
redirectAttributes.addFlashAttribute("result", "modified");
//수정한 게시물 번호 전송
redirectAttributes.addAttribute("bno", boardDTO.getBno());
//수정 완료시 조건없이 read 페이지로 이동
return "redirect:/board/read";
}
2. 화면 작성
(1) modify.html 작성
-read.html에서 제목과 내용을 readonly으로 변경
-목록/수정/삭제 버튼 생성
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:th="http://www.thymeleaf.org"
layout:decorate="~{layout/basic.html}">
<head>
<meta charset="UTF-8">
<title>Board Register-title</title>
</head>
<div layout:fragment="content">
<div class="row mt-3">
<div class="col">
<div class="card">
<!--굵음-->
<div class="card-header">
Board Register-header
</div>
<!-- 카드 본문에 <form>태그로 게시물에 입력할 항목 추가-->
<div class="card-body">
<form action="/board/modify" method="post" id="f1">
<!-- 글번호-->
<div class="input-group mb-3">
<div class="input-group-text">Bno</div>
<input type="text" class="form-control" th:value="${dto.Bno}" name="bno" readonly>
</div>
<!--제목 -->
<div class="input-group mb-3">
<div class="input-group-text">Title</div>
<input type="text" class="form-control" th:value="${dto.title}"name="title">
</div>
<!--내용 -->
<div class="input-group mb-3">
<div class="input-group-text">Content</div>
<textarea class="form-control col-sm-5" rows="5" name="content">[[${dto.content}]]</textarea>
</div>
<!--작성자-->
<div class="input-group mb-3">
<div class="input-group-text">Writer</div>
<input type="text" class="form-control" th:value="${dto.writer}" name="writer" readonly>
</div>
<!--등록시간-->
<div class="input-group mb-3">
<div class="input-group-text">regDate</div>
<input type="text" class="form-control" th:value="${#temporals.format(dto.regDate, 'yyyy-MM-dd HH:mm:ss')}" readonly>
</div>
<!--수정시간-->
<div class="input-group mb-3">
<div class="input-group-text">modDate</div>
<input type="text" class="form-control" th:value="${#temporals.format(dto.modDate, 'yyyy-MM-dd HH:mm:ss')}" readonly>
</div>
<!--목록/수정/삭제 버튼-->
<div class="my-4">
<div class="float-end">
<button type="button" class="btn btn-primary listBtn">List</button>
<button type="button" class="btn btn-secondary modBtn">Modify</button>
<button type="button" class="btn btn-danger removeBtn">Remove</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
(2) 버튼 이벤트 처리 및 에러 메시지 처리
HistoryAPI에서 제공하는 목록에 주소를 추가하기 위한 메소드
-history.state를 이용해 추가한 주소에 접근이 가능하다.
-history.pushState() : 주소 목록에 새로운 주소를 추가하므로, 이전 주소가 남아있다.
history.pushState({ data: 'pushpush' }, 'title을 pushState로', '/pushpush')
-history.replaceState() : 이전 주소를 없애고 바꿀 주소로 대체한다.
history.replaceState({ data: 'replace' }, 'title을 replaceState로', '/replace');
https://www.zerocho.com/category/HTML&DOM/post/599d2fb635814200189fe1a7
-수정 버튼 이벤트 처리
-에러 발생시 에러메시지 반환 이벤트 처리
<script layout:fragment="script" th:inline="javascript">
//에러 메시지 처리
const errors=[[${errors}]]
console.log(errors)
let errorMsg=''
//null이 아니라면, 즉 존재한다면
if(errors){
for(let i=0;i<errors.length;i++){
errorMsg+=`${errors[i].field}은(는) ${errors[i].code} \n`
}
//히스토리에 추가
history.replaceState({}, null, null)
alert(errorMsg)
}
//버튼의 클래스 속성 값을 이용해 이벤트 처리
const link = [[${pageRequestDTO.getLink()}]]
const formObj = document.querySelector("#f1")
document.querySelector(".modBtn").addEventListener("click", function(e){
e.preventDefault()
e.stopPropagation()
formObj.action = `/board/modify?${link}`
formObj.method ='post'
formObj.submit()
}, false)
</script>
삭제 처리
1. 컨트롤러에 POST방식의 remove() 작성
@PostMapping("/remove")
public String remove(Long bno, RedirectAttributes redirectAttributes){
log.info("remove post..."+bno);
boardService.remove(bno);
redirectAttributes.addFlashAttribute("result", "removed");
return "redirect:/board/list";
}
2. 버튼 이벤트 추가
- 삭제 버튼
- 목록 버튼
-modify.html에 자바스크립트에서 <form>태그를 이용해 '/board/remove' 호출하도록 추가
document.querySelector(".removeBtn").addEventListener("click", function(e){
e.preventDefault()
e.stopPropagation()
formObj.action = `/board/remove`
formObj.method ='post'
formObj.submit()
}, false)
-페이지/검색 조건을 유지한 채 목록으로 이동하도록 추가
document.querySelector(".listBtn").addEventListener("click", function(e){
e.preventDefault()
e.stopPropagation()
formObj.reset()
self.location =`/board/list?${link}`
}, false)