본문 바로가기

Server Programming/Spring Boot Backend Programming

5장-5. 게시물 관리 프로젝트 구현 (2) CRUD (+ #temporals, #number, HistoryAPI)

반응형

요구 사항

  • 목록 처리
  • 등록 처리
  • 조회 처리
  • 수정 처리
  • 삭제 처리

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을 전달

-> 즉, 화면으로 PageRequestDTOPageResponseDTO가 전달

 

2. 화면 작성

- 부트스트랩의 card를 감싸는 여러개의 <div> 추가해, <div>를 이용하는 방식으로 화면 구성

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>

(1) list.html 작성

  • div:row mt-3
    • div:col
      • div:card
        • div:card-body
        • h5:card-title
          • table
            • thead
              • tr
                • th : col
            • tbody
              • tr
                • th : row
                • td
<!-- 타임리프 레이아웃을 적용해 본문만 적용-->
<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/

 

페이지네이션

여러 페이지에 일련의 관련 내용이 있음을 나타내는 페이지네이션을 사용한 문서와 예시입니다.

getbootstrap.kr

 

https://getbootstrap.kr/docs/5.1/utilities/flex/

 

플렉스

반응형 flexbox 유틸리티 세트를 사용해 그리드 열, 네비게이션 바, 컴포넌트, 레이아웃의 정렬 및 크기 조정을 신속하게 관리합니다. 더 복잡한 구현의 경우 사용자 정의 CSS가 필요할 수 있습니다

getbootstrap.kr

 

aria-label을 사용하여 요소에 액세스 가능한 이름을 명시적으로 설정

https://getbootstrap.kr/docs/5.2/forms/overview/#%EC%A0%91%EA%B7%BC%EC%84%B1

 

다양한 폼 사용을 위한 폼 컨트롤 스타일, 레이아웃 옵션, 사용자 정의 컴포넌트의 예시와 사용 가이드입니다.

getbootstrap.kr

 

페이지 번호 출력 기본 문법

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

 

입력 그룹

텍스트 입력, 사용자 정의 셀렉트, 사용자 파일 선택 등의 폼의 좌우에 텍스트, 버튼 혹은 버튼 그룹을 추가해 폼 컨트롤을 간단하게 확장할 수 있습니다.

getbootstrap.kr

 

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/

 

카드

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

getbootstrap.kr

 

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

 

플로트

반응형 float 유틸리티를 사용하여 모든 중단점에서 모든 요소에서 float을 전환합니다.

getbootstrap.kr

  <!-- 카드 본문에 <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/

 

모달

Bootstrap JavaScript 모달 플러그인을 사용하여 라이트박스, 사용자 알림 또는 사용자 정의 콘텐츠를 만들 수 있습니다.

getbootstrap.kr

 

-정상 등록 시, '/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

 

텍스트

정렬, 줄바꿈, 굵기 등을 제어하는 ​​일반적인 텍스트 유틸리티에 대한 문서 및 예제입니다.

getbootstrap.kr

 

 <!--목록 / 수정 버튼-->                                                                                 
  <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

 

https://www.zerocho.com/category/HTML&DOM/post/599d2fb635814200189fe1a7

 

www.zerocho.com

 

-수정 버튼 이벤트 처리

-에러 발생시 에러메시지 반환 이벤트 처리

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

 

반응형