1.  AJax와 JASON : 댓글 기능 구현 

기존 작업한 게시판에 댓글 기능을 구현
클라이언트는 Ajax(Axios 라이브러리) · 서버는 RESTful · JPA는 다대일 방식으로 구현


(1)  Ajax와 REST 방식의 이해

 

1)  Ajax Asynchronous JavaScript And XML


  💫  Ajax 방식은 브라우저에서 서버를 호출하지만 모든 작업이 브라우저 내부에서 이루어지기 때문에 현재 브라우저의 브라우저 화면 변화 없이 서버와 통신할 수 있음

 

    ✓  실제 구현은 자바 스크립트를 이용해서 XML을 주고 받는 방식을 이용
    ✓  최근에는 JSON을 이용하는 방식을 더 선호. 스프링 부트는 Springweb을 추가하면 자동으로 관련 라이브러리를 받음

  💫  클라이언트 중심의 개발

    ✓  Ajax가 가져온 변화는 모바일에서도 Ajax 방식으로 데이터를 교환할 수 있기 때문에 활용이 커짐
    ✓  모바일에서도 일반 웹과 마찬가지로 서버의 데이터가 필요한데 이때 화면과 관련된 부분은 필요하지 않기 때문에 서버에서 순수한 데이터만 전송하는 방식이라면 클라이언트의 구현이 웹 / 앱에 관계없이 데이터를 재사용할 수 있음

 

 

2)  JSON 문자열

 

    📍 "서버에서 순수한 데이터를 보내고 클라이언트가 적극적으로 이를 처리한다"라는 개발 방식에서 핵심은 '문자열'

  • '문자열'은 어떠한 프로그래밍 언어나 기술에 종속되지 X
  • 문자열을 이용하면 데이터를 주고 받는 것에 신경 써야 하는 일은 없지만, 문자열로 복잡한 구조의 데이터를 표현하는데 문제가 발생
  • 문자열로 복잡한 데이터를 표현하기 위해서 고려되는 것이 XML과 JSON 이라는 형태.
  • JSON은 자바스크립트 문법에 맞는 문자열로 데이터를 표현하기 때문에 클라이언트에서 어떤 기술을 이용하든 공통적으로 인식할 수 있음
  • 스프링 부트 역시 JSON 관련 라이브러리 jackson-databind가 이미 포함되어 있으므로 별도의 설정 없이 바로 JSON 데이터를 만들어 낼 수 있음

 

3)  REST 방식

 

  💫  html이 아니라 json or xml 형식이 response에 사용

    ✓  REST 방식은 클라이언트 프로그램인 브라우저나 앱이 서버와 데이터를 어떻게주고 받는 것이 좋을지에 대한 가이드
    ✓  예전의 웹 개발 방식에서는 특정한 URL이 원하는 '행위나 작업'을 의미하고, GET / POST 등은 데이터를 전송하는 위치를 의미
      Ajax를 이용하면 브라우저의 주소가 이동할 필요 없이 서버와 데이터를 교환할수 있기 때문에 URL은 '행위나 작업'이 아닌 '원하는 대상' 그 자체를 의미하고, GET / POST 방식과 PUT / DELETE 등의 추가적인 전송방식을 활용해서 '행위나 작업'을 의미하게 됨

 

이전 표현 REST 방식 표현
/board/modify  ▶️  게시물의 수정 (행위 / 목적)
<form>  ▶️  데이터의 묶음
 /board/123  ▶️  게시물 지원 자체
PUT 방식  ▶️  행위나 목적



4)  REST 방식의 URL 설계

Method URI 의미 추가 데이터
GET /board/123 123번 게시물 조회  
POST /board/ 새로운 게시물 등록 신규 데이터 게시물
PUT /board/123 123번 게시물 수정 수정에 필요한 데이터
DELETE /board/123 123번 게시물 삭제  

 

 

5) springdoc-openapi 준비

 

  ⚡️  REST 방식의 테스트는 특별한 화면을 구성하는 것이 아니라 데이터를 전송하고 결과를 확인하는 방법
          ➡️  예를 들어 브라우저는 GET 방식의 데이터를 확인할 때는 유용하지만 POST 방식으로 데이터를 처리할 때는 상당히 불편하고, 사용자도 측정한 경로를 어떻게 호출하는지 알 수 없으므로 상세한 정보를 전달하기 어려운 단점이 있음
  ⚡️  REST 방식을 이용할 때는 전문적으로 API를 테스트 할 수 있는 Postman이나 springdoc-openapi 등을 이용

 

Swagger UI
  • 부트 2까지는 Swagger UI를 사용. 부트 3부터는 Swagger UI사용이 안되고 springdoc-openapi를 지원
  • openapi는 개발할 때 어노테이션 설정으로 API 문서와 테스트 할 수 있는 화면을 생성할 수 있으므로 개발자는 한번에 테스트 환경까지 구성할 수 있다는 장점이 있음

(2)  프로젝트의 준비

1)  modelMapper 설정 변경

  • RootConfig의 modelMapper 설정을 변경. STRICT ▶️ LOOSE로 변경
@Configuration
public class RootConfig {
    @Bean
    public ModelMapper getMapper() {
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration()
            .setFieldMatchingEnabled(true)
            .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
            .setMatchingStrategy(MatchingStrategies.LOOSE);
       return modelMapper;
    }
}

 

 

2) bulid.gradle 설정 추가

  • 프로젝트의 bulid.gradle 파일에 'OpenAPI Starter WebMVC UI'으로 검색한 라이브러리를 추가
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'

 

  • 서버 실행을 하고 아래 주소로 접근
    http://localhost:8080/swagger-ui/index.html

 

3) Config 클래스 추가

  • 프로젝트의 config 패키지에 SpringdocOpenapiConfig 클래스를 추가
  • 일반 컨트롤러와 REST 컨트롤러를 분리해서 설정
  • 경로에 /api가 포함된 컨트롤러의 경우에는 REST API로 인식
  • 경로에 /api가 포함안된 컨트롤러의 경우에는 COMMON API로 인식
@Configuration
public class SpringdocOpenapiConfig {

    @Bean
    public GroupedOpenApi restApi() {
        return GroupedOpenApi.builder()
                .pathsToMatch("/api/**")
                .group("REST API")
                .build();
    }

    @Bean
    public GroupedOpenApi commonApi() {
        return GroupedOpenApi.builder()
                .pathsToMatch("/**/*")
                .pathsToExclude("/api/**/*")
                .group("COMMON API")
                .build();
    }
}

 

SampleJasonController 경로 수정
@Log4j2
@RestController
public class SampleJSONController {

    @GetMapping("/api/helloArr")
    public String[] helloArr() {
        log.info("HelloArr...");

        return new String[] {"AAA", "BBB", "CCC"};
    }
}

SampleController에 @Operation 추가
@Operation(summary = "hello")
@GetMapping("/hello")
public void hello(Model model) {
    log.info("hello...");
    model.addAttribute("msg", "Hello World");
}

@Operation(summary = "ex/ex1")
@GetMapping("/ex/ex1")
public void ex1(Model model) {
    List<String> list = Arrays.asList("AAA", "BBB", "CCC", "DDD");

    model.addAttribute("list", list);
}


 

  ⚡️  현재 설정은 기본적으로 가장 많이 사용하는 패키지를 기반으로 패키지의 모든 클래스에 대해서 API 테스트 환경을 만들어 냄

 

 

 

 ✓  화면의 board-controller를 선택하면 다음과 같이 메서드를 선택할 수 있는 화면이 나옴

✓  특정한 메서드를 선택하고 'Try it out' 버튼을 클릭하면 파라미터를 입력할 수 있음

✓  파라미터를 입력하고 'Execute'를 클릭하면 아래에서 결과 화면을 볼수 있음

 

 

 

 

 

 

 

정적 파일 경로 문제


  ✓  Swagger UI 설정이 끝나면 기존에 동작하던 /board/list/ 에는 문제가 생길 수 있음

  ✓  이 문제를 해결하려면 Spring Web 관련 설정을 추가해 주어야만 함
  ✓  config 폴더에 CustomServletConfig 클래스를 추가

 

  • 클래스에 @EnableWebMvc 어노테이션을 추가하는 것이 가장 중요
  • Swagger UI 가 적용되면서 정적 파일의 경로가 달라졌기 때문인데 이를 CustomServletConfig로 WebMvcConfigurer 인터페이스를 구현하도록하고 addResourceHandlers를 재정의해 수정
@Configuration
@EnableWebMvc
public class CustomServletConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/");
        registry.addResourceHandler("/fonts/**").addResourceLocations("classpath:/static/fonts/");
        registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/");
        registry.addResourceHandler("/assets/**").addResourceLocations("classpath:/static/assets/");
    }
}

 

 

 

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


1.  게시물의 수정/삭제 처리

게시물 수정/삭제에 대한 처리는 GET 방식으로 게시물을 수정이나 삭제할 수 있는 화면을 보는 것으로 시작

BoardController의 read()를 수정해서 /board/modify에도 적용되도록 수정
@GetMapping({"/read", "/modify"})
public void read(Long bno, PageRequestDTO pageRequestDTO, Model model) {
    BoardDTO boardDTO = boardService.readOne(bno);
    log.info(boardDTO);
    model.addAttribute("dto", boardDTO);
}

 

templates의 board 폴더에는 modify.html을 추가
<!DOCTYPE html>
<html lang="en"
      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 Modify</title>
</head>

<div layout:fragment="content">
    <div class="row mt-3">
        <div class="col">
            <div class="card">
                <div class="card-header">
                    Board Modify
                </div>
                <div class="card-body">
                
                </div> <!-- end card-body -->
            </div> <!-- end card -->
        </div> <!-- end col -->
    </div> <!-- end row -->
</div>

 

<form> 태그를 추가하고 'dto'라는 이름으로 전달된 BoardDTO 객체를 출력

 

  📍  <form> 태그에 제목 title, 내용 contents 을 수정할 수 있도록 작성하고 전송할 때 필요한 name 속성을 확인

<div class="card-body">
    <form th:action="@{/board/modify}" method="post" id="f1">
        <div class="input-group mb-3">
            <span class="input-group-text">Bno</span>
            <input type="text" class="form-control" name="bno" th:value="${dto.bno}" readonly>
        </div>

        <div class="input-group mb-3">
            <span class="input-group-text">Title</span>
            <input type="text" class="form-control" name="title" th:value="${dto.title}" >
        </div>

        <div class="input-group mb-3">
            <span class="input-group-text">Content</span>
            <textarea class="form-control col-sm-5" name="content" rows="5" >[[${dto.content}]]</textarea>
        </div>

        <div class="input-group mb-3">
            <span class="input-group-text">Writer</span>
            <input type="text" class="form-control" name="writer" th:value="${dto.writer}" readonly>
        </div>

        <div class="input-group mb-3">
            <span class="input-group-text">RegDate</span>
            <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">
            <span class="input-group-text">ModDate</span>
            <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> <!-- end card-body -->

 

/board/modify?bno=310 형태로 수정화면 접근


1)  수정 처리

  🚀  실제 수정 작업은 POST 방식으로 처리. 수정 후에는 다시 조회 화면으로 이동해서 수정된 내용을 확인할 수 있도록 구현
  🚀  수정 후 조회 화면으로 이동시에 검색했던 조건들이 해당하지 않을 수 있어서, 수정 후에는 검색 조건 없이 단순 조회 화면으로 이동하도록 구현

BoardController에는 POST 방식의 modify()를 구현
@PostMapping("/modify")
public String modify(PageRequestDTO pageRequestDTO,
                     @Valid BoardDTO boardDTO,
                     BindingResult bindingResult,
                     RedirectAttributes redirectAttributes) {

    log.info("board POST modify..." + boardDTO);

    if (bindingResult.hasErrors()) {
        log.info("has errors......");

        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());
    return "redirect:/board/read";
}

 

  ✓  modify()에는 문제가 발생할 때 'errors'라는 이름으로 다시 수정 페이지로 이동 할 수 있도록 PageRequestDTO의 getLink()를 통해서 기존의 모든 조건을 붙여서 /board/modify'로 이동하게 구성.
  ✓  수정 작업에 이상이 없을 때는 아무런 검색이나 페이징 조건없이 /board/read로 이동

 

modify.html에 문제가 있을 때는 자바스크립트를 이용해서 처리하도록 구성
<script layout:fragment="script" th:inline="javascript">
    const errors = [[${errors}]]
    console.log(errors)

    let errorMsg = ''

    if(errors) {
        for (let i = 0; i < errors.length; i++) {
            errorMsg += `${errors[i].field}은(는) ${errors[i].code} \n`;
        }
        history.replaceState({}, null, null)
        alert(errorMsg);
    }
</script>

 

자바스크립트로 이벤트 처리하는 부분을 작성
    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);

 

  ✓  수정 처리를 할 때는 나중에 잘못되는 상황을 대비해서 페이지 / 검색 정보도 같이 쿼리 스트링으로 전달

 


2)  삭제 처리

  🚀  BoardController에서 remove()라는 메서드 구성, 삭제 후에는 목록으로 이동하도록 구성

@PostMapping("/remove")
public String remove(Long bno, RedirectAttributes redirectAttributes) {
    log.info("remove()...");

    boardService.remove(bno);
    redirectAttributes.addFlashAttribute("result", "removed");
    return "redirect:/board/list";
}

 

modify.html에는 자바스크립트를 이용해서 /board/remove를 호출하도록 작성
document.querySelector(".removeBtn").addEventListener("click", function(e){

    e.preventDefault();
    e.stopPropagation();

    formObj.action = `/board/remove`;
    formObj.method = 'post';
    formObj.submit();

}, false);

 

  ✓ 정상적으로 삭제가 이루어질 경우 RedirectAttributes를 통해서 'result' 값이 전달되므로 list.html에서는 모달창으로 삭제 처리된 것을 알 수 있음

 

 

목록으로 이동하는 버튼의 이벤트 처리는 페이지/검색 조건을 유지하도록 구성
document.querySelector(".listBtn").addEventListener("click", function(e){
    e.preventDefault();
    e.stopPropagation();

    formObj.reset();
    self.location = `board/list?${link}`;
}, false);

 

 

 

 

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


1.  조회 처리와 화면 개발

BoardController에 특정한 번호의 게시물을 조회하는 기능을 추가
조회할 때는 PageRequestDTO를 같이 활용해서 나중에 목록 화면으로 이동이 가능하도록 구성

@GetMapping("/read")
public void read(Long bno, PageRequestDTO pageRequestDTO, Model model) {
    BoardDTO boardDTO = boardService.readOne(bno);
    log.info(boardDTO);
    model.addAttribute("dto", boardDTO);
}

 

templates의 board 폴더에는 read.html을 구성
<!DOCTYPE html>
<html lang="en"
      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>
</head>

<div layout:fragment="content">
    <div class="row mt-3">
        <div class="col">
            <div class="card">
                <div class="card-header">
                    Board Read
                </div>
                <div class="card-body">
                
                </div><!--end card body-->
            </div> <!-- end card -->
        </div> <!-- end col -->
    </div> <!-- end row -->
</div>

 

브라우저에서 /board/read?bno=310 같이 존재하는 번호로 조회

 

BoardController에서 Model에 'dto'라는 이름으로 전달되는 BoardDTO를 출력하는 부분을 <div class="card-body"> 내에 작성
<div class="card-body">
    <div class="input-group mb-3">
        <span class="input-group-text">Bno</span>
        <input type="text" class="form-control" th:value="${dto.bno}" readonly>
    </div>
    
    <div class="input-group mb-3">
        <span class="input-group-text">Title</span>
        <input type="text" class="form-control" th:value="${dto.title}" readonly>
    </div>

    <div class="input-group mb-3">
        <span class="input-group-text">Content</span>
        <textarea class="form-control col-sm-5" rows="5" readonly>[[${dto.content}]]</textarea>
    </div>

    <div class="input-group mb-3">
         <span class="input-group-text">Writer</span>
         <input type="text" class="form-control" th:value="${dto.writer}" >
    </div>

    <div class="input-group mb-3">
         <span class="input-group-text">RegDate</span>
         <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">
         <span class="input-group-text">ModDate</span>
         <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>


2.  목록 페이지로 이동

read.html의 마지막 부분에 버튼을 통해서 이동하는 부분이 존재. 이에 대한 처리는 각 버튼을 감싸는 <a>태그를 이용해서 PageRequestDTO의 getLink()를 활용

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


1)  목록에서 조회 링크 처리

목록 화면에서 게시물을 선택해 이동하도록 처리
목록을 반복문으로 처리하는 부분에서 th:with를 이용해 PageRequestDTO의 getLink() 결과를 변수로 처리하고 나머지 링크를 완성

<tbody th:with="link = ${pageRequestDTO.getLink()}">
    <tr th:each="dto:${responseDTO.dtoList}">
        <th scope="col">[[${dto.bno}]]</th>
        <td><a th:href="|@{/board/read(bno =${dto.bno})}&${link}|"> [[${dto.title}]] </a></td>
        <td>[[${dto.writer}]]</td>
        <td>[[${#temporals.format(dto.regDate, 'yyyy-MM-dd')}]]</td>
    </tr>
</tbody>

 

  ✓  완성된 후에는 페이지나 검색 조건을 유지된 채로 목록에서 조회, 조회에서 목록으로 이동이 가능

 

 

 

 

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


1.  등록 처리와 화면 개발

등록 처리 시에는 @Valid를 이용해서 서버에서도 검증한 후에 등록하는 방식을 적용

@Valid를 위해서 bulid.gradle에 validation 관련 라이브러리를 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'

 

BoardDTO에는 제목이나 내용, 작성자 등이 비어 있지 않도록 어노테이션들을 처리
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)  BoardController 처리

  🚀  등록 처리는 GET 방식으로 화면을 보고 POST 방식으로 처리
  🚀  @Valid에서 문제가 발생했을 때 모든 에러를 errors라는 이름으로 RedirectAttributes에 추가해서 전송

@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("has errors...");
        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";
}
  💡  addFlashAttribute()
      -  Spring Framework에서 제공하는 메소드로, 주로 리다이렉트 시 데이터를 일시적으로 저장하는 데 사용
      -  Flash 속성은 리다이렉트 이후에만 유효하며, 한 번 읽히면 소멸하는 특성이 있음
      -  이를 통해 사용자는 브라우저를 새로고침하거나 직접 URL을 입력해도 해당 데이터가 유지되지 않도록 할 수 있음

 


2) register.html 처리

  🚀  templates/board 폴더에 register.html을 추가

<!DOCTYPE html>
<html lang="en"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
    layout:decorate="~{layout/basic.html}">
<head>
    <meta charset="UTF-8">
    <title>Board Register</title>
</head>

<div layout:fragment="content">
    <div class="row mt-3">
        <div class="col">
            <div class="card">
                <div class="card-header">
                    Board Register
                </div>
                <div class="card-body">

                </div>
            </div>
        </div>
    </div>
</div>

<script layout:fragment="script" th:inline="javascript">
</script>

 

 

  🚀  <div class="card-body"> 부분에는 <form> 태그를 이용해서 입력할 수 있는 부분을 추가
  🚀  게시물 등록에는 제목 title, 내용 content, 작성자 writer가 추가

<div class="card-body">
<form action="/board/register" method="post">
    <div class="input-group mb-3">
        <span class="input-group-text">Title</span>
        <input type="text" name="title" class="form-control" placeholder="Title">
    </div>

    <div class="input-group mb-3">
        <span class="input-group-text">Content</span>
        <textarea class="form-control col-sm-5" rows="5" name="content"></textarea>
    </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><!-- end card body -->


3) @Valid의 에러 메서드 처리

  🚀  등록은 @Valid를 통해서 검증하므로 검증에 실패하면 다시 앞의 화면으로 이동
  🚀  이때 addFlashAttribute()를 통해서 'errors' 라는 이름으로 에러 메시지들이 전송됨

<script layout:fragment="script" th:inline="javascript">
    const errors = [[${errors}]];
    console.log(errors);

    let errorMsg = '';

    if(errors) {
        for (let i = 0; i < errors.length; i++) {
            errorMsg += `${errors[i].field}은(는) ${errors[i].code} \n`;
        }
        alert(errorMsg);
    }
</script>

 

  💡  Thymeleaf의 인라인 기능을 이용하면 'errors'를 자바 스크립트의 배열로 처리할 수 있기 때문에 이를 이용해서 앞의 메시지를 작성할 수 있음
  💡  브라우저를 통해서 아무것도 입력하지 않은 상태에서 submit이 실행되면 경고창이 보임
  💡  콘솔창에는 배열로 만들어진 에러 메시지를 출력

 


4)  정상적인 처리와 모달창

  🚀  등록 화면에서 필요한 내용들이 추가되면 정상적으로 '/board/list'로 이동하는 것을 확인
  🚀  목록 화면으로 이동했을 때 BoardController에서 RedirectAttributes의 addFlashAttribute()를 이용해서 'result'라는 데이터를 추가적으로 전달. addFlashAttribute()로 전달된 데이터는 쿼리 스트링으로 처리되지 않기 때문에 브라우저의 경로에는 보이지 않음
  🚀  list.html에서는 이렇게 전달된 result 변수를 이용해서 모달창으로 처리

 

list.html의 <script> 태그의 마지막 부분에 코드 추가
// show modal
const result = [[${result}]];

if(result) {
    modal.show();
}

경고창이 동작한 것을 확인후에 모달창을 추가
- 모달창의 코드는 부트스트랩의 Componets 메뉴를 통해서 확인

 

  ✓  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>
// show modal
const result = [[${result}]];

const modal = new bootstrap.Modal(document.querySelector(".modal"));

if(result) {
    modal.show();
}

 

  ✓  변경된 코드가 적용되면 등록 후에는 모달창이 보이게 되고, 직접 '/borad/list'로 접근할 때는 모달창이 보이지 않게 됨
  ✓  이를 이용해서 사용자에게 어떠한 처리가 완료 되었는지에 대한 결과를 알려줄 수 있음

 

 

 

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


1.  컨트롤러와 화면 처리

프로젝트 controller 패키지에 BoardController 구성

 

    🚀  가장 우선적으로 구현해야 하는 기능은 목록 기능이므로 list() 메서드를 추가하고 PageRequestDTO를 이용해서 페이징 처리와 검색에 이용

@Controller
@RequestMapping("/board")
@Log4j2
@RequiredArgsConstructor
public class BoardController {
    private final BoardService boardService;

    @GetMapping("/list")
    public void list(PageRequestDTO pageRequestDTO, Model model) { }
}

 


1)  화면 구성을 위한 준비

화면 구성은 Thymeleaf를 이용해서 레이아웃을 적용할 수 있도록 준비. build.gradle 파일에 Thymeleaf의 레이아웃 관련 라이브러리의 존재 여부를 먼저 확인

implementation group: 'nz.net.ultraq.thymeleaf', name: 'thymeleaf-layout-dialect', version: '3.1.0'

 


2)  템플릿 디자인 적용

Thymeleaf에는 레이아웃 기능을 적용할 수 있어서 본격적인 디자인을 적용. 부트스트랩의 여러 무료 디자인 템플릿 중 'Simple Sidebar'를 이용

https://startbootstrap.com/template/simple-sidebar

 

Start Bootstrap

 

startbootstrap.com

 

  • 무료로 이용할 수 있으므로 내려받은 후에 압축을 해제하고 모든 파일을 resources 의 static 폴더에 복사
  • 프로젝트를 실행하고 브라우저에 'http://localhost:8080/'를 실행했을 때 index.html 파일이 실행되는지 확인
  • 적용한 템플릿이 반응형이라 브라우저의 크기에 따라 다르게 보임
Thymeleaf 레이아웃으로 변경하기

 

    ✓  레이아웃을 적용하려면 layout 폴더에 basic.html을 추가하고 index.html 내용을 그대로 복사해서 추가

 

레이아웃 적용

 

    ✓  basic.html의 상단에는 Thymeleaf의 네임스페이스들을 추가

<html lang="en"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:th="http://www.thymeleaf.org">

 

    ✓  <head> 태그에는 링크가 존재하므로 이를 Thymeleaf 스타일로 변경. 경로를 '@{/..}'와 같은 형태로 처리. ('/'로 경로가 시작하는 것을 주의)

    <link rel="icon" type="image/x-icon" th:href="@{/assets/favicon.ico}" />
    <!-- Core theme CSS (includes Bootstrap)-->
    <link th:href="@{/css/styles.css}" rel="stylesheet" />
</head>

 

    ✓  'Page content' 부분에 layout:fragment를 적용

<!-- Page content-->
<div class="container-fluid" layout:fragment="content">

 

    ✓  파일의 마지막 부분에 자바 스크립트를 위한 <th:block>을 추가하고 링크를 수정

<script th:src="@{/js/scripts.js}"></script>
    <th:block layout:fragment="script">
    
    </th:block>

 

컨트롤러를 통한 확인

 

    ✓  BoardController에는 list() 기능을 작성해 두었으므로 이를 활용해서 레이아웃까지 적용된 화면을 구성
    ✓  templates 폴더에 board 폴더를 생성하고 list.html을 추가

    ✓  list.html은 레이아웃 적용여부를 확인할 수 있도록 작성.

<html lang="en"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:th="http://www.thymeleaf.org"
      layout:decorate="~{layout/basic.html}">

<div layout:fragment="content">
    <h1>Board List</h1>
</div>

<script layout:fragment="script" th:inline="javascript">
    console.log('script...');
</script>

   


2.  목록 화면 개발

1)  BoardController

  🚀  BoardController의 list()에 화면에 목록 데이터를 출력하는 코드 작성

  🚀  list()가 실행되면 PageRequestDTO와 PageResponseDTO 객체가 화면으로 전달

@GetMapping("/list")
public void list(PageRequestDTO pageRequestDTO, Model model) {
    PageResponseDTO<BoardDTO> responseDTO = boardService.list(pageRequestDTO);

    log.info(responseDTO);

    model.addAttribute("responseDTO", responseDTO);
}

 


2)  list.html 수정

  🚀  화면 상단에는 여러개의 <div>를 이용해서 화면 구성을 처리

  🚀  실제 내용물의 출력은 <tbody>를 이용하고 Thymeleaf의 반복문을 이용해서 처리

<div layout:fragment="content">
    <div clsss="col">
        <div class="card">
            <div class="card-header">
                Board List
            </div>
            <div class="card-body">
                <h5 class="card-title">Board List</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.list}">
                        <th scope="col">[[${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 -->

 


3)  날짜 포멧팅 처리

  🚀  화면의 등록일 regDate 이 너무 길고 상세하게 나오는 것을 날짜만 나오도록 처리
  🚀  Thymeleaf의 #temporals라는 유틸리티 객체를 이용해서 처리

<td>[[${#temporals.format(dto.regDate, 'yyyy-MM-dd')}]]</td>


4)  페이지 목록의 출력

  🚀  <table> 태그가 끝나는 부분과 이어지게 <div>를 구성해서 페이지 번호들을 화면에 출력
  🚀  PageResponseDTO는 시작 번호 start와 끝 번호 end 만을 가지고 있으므로 특정 범위의 숫자를 만들기 위해서 Thymeleaf의 numbers를 이용

<div class="float-end">
    <ul class="pagination flex-wrap">
        <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="i: ${#numbers.sequence(responseDTO.start,responseDTO.end)}">
            <li th:class="${responseDTO.page == i} ? 'page-item active' : 'page-item'">
                <a class="page-link" th:data-num="${i}">[[${i}]]</a>
            </li>
        </th:block>
        <li class="page-item" th:if="${responseDTO.next}">
            <a class="page-link" th:data-num="${responseDTO.end + 1}">Next</a>
        </li>
    </ul>
</div>

 

  📍  코드에서 중요한 부분은 #numbers.sequence()로 특정한 범위의 연속된 숫자를 만드는 부분과 <a> 태그에 'data-num'이라는 속성으로 페이지 처리하는 부분

  📍  브라우저에서 직접 URL을 변경하는 방법으로 '/board/list?page=2'와 같이 페이지 번호를 쿼리 스트링으로 추가해서 페이지 변경되는 것을 확인

 


5) 검색 화면 추가

  🚀  list.html 페이지에는 검색이 이루어질수 있도록 <table> 위에 별도의 card 영역을 만들어 검색 조건을 선택할 수 있도록 구성.

  🚀  검색 조건은 페이지 이동과 함께 처리될 수 있도록 <form> 태그로 감싸서 처리

<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-prepend">
                    <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}">
                <div class="input-group-append">
                    <button class="btn btn-outline-secondary searchBtn" type="submit">Search</button>
                    <button class="btn btn-outline-secondary clearBtn" type="button">Clear</button>
                </div>
            </div>
        </div>
    </form>
</div>

 

  📍  브라우저에 /board/list 뒤에 type과 keyword를 지정하면 화면에서 검색 항목과 키워드 부분으로 처리 되는것 확인

 


6)  이벤트 처리

  🚀  페이지 번호를 클릭하거나 검색/필터링 조건을 눌렀을 때 이벤트 처리를 추가

  • 페이지 번호를 클릭하면 검색 창에 있는 <form> 태그에 <input type='hidden'>으로 page를 추가한 후에 submit
  • Clear 버튼을 누르면 검색 조건 없이 /board/list 호출

  🚀  JSP에서 자바스크립트의 문자열을 템플릿으로 적용하고자 EL과 구분하기 위해 \${} 와 같은 방식을 사용했지만, Thymeleaf는 \ 없이 적용한다는 점을 제외하면 기존 코드를 그대로 사용

list.html의 마지막 부분에 <script> 영역을 작업
<script layout:fragment="script" th:inline="javascript">
    document.querySelector(".pagination").addEventListener("click", function(e) {
        e.preventDefault(); // a 태그일 경우 기본 이벤트 제거
        e.stopPropagation();

        const target = e.target;

        if(target.tagName !== 'A') { // a 태그가 아니면 종료
            return;
        }

        const num = target.getAttribute("data-num"); // 클릭한 a 태그의 data-num 속성(이동해야할 페이지번호)를 가져옴.

        const formObj = document.querySelector("form");

        formObj.innerHTML += `<input type='hidden' name='page' value='${num}'>`; // form 태그에 page 태그 추가

        formObj.submit();

    }, false)

    document.querySelector('.clearBtn').addEventListener("click", function(e){
        e.preventDefault();
        e.stopPropagation();

        self.location = '/board/list';

    }, false)
</script>

 

 

 

 

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


1.  서비스 계층과 DTO의 구현

BoardRepository의 모든 메서드는 서비스 계층을 통해서 DTO로 변환되어 처리되도록 구성
엔티티 객체는 영속 컨텍스트에서 관리되므로 가능하면 많은 계층에서 사용되지 않는 것이 좋음

'서비스 계층에서 엔티티 객체를 DTO로 변환하거나 반대의 작업'을 처리하도록 함.
  ➡️  이 때 ModelMapper를 이용


1)  ModleMapper 설정

DTO와 엔티티 간의 변환 처리를 간단히 처리하기 위해서 ModelMapper를 이용할 것이므로 build.gradle 파일에 ModelMapper 라이브러리가 존재하는지 우선 확인

implementation 'org.modelmapper:modelmapper:3.1.0'

 

  • 프로젝트에 config 패키지를 구성하고 RootConfig 클래스를 구성
  • @Confuguration을 이용해서 해당 클래스가 스프링의 설정 클래스임을 명시
  • @Bean을 이용해서 ModelMapper를 스프링의 빈으로 설정
@Configuration
public class RootConfig {
    @Bean
    public ModelMapper getMapper() {
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration()
                .setFieldMatchingEnabled(true)
                .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
                .setMatchingStrategy(MatchingStrategies.STRICT);
        return modelMapper;
    }
}

 


 

2) CRUD 작업 처리

프로젝트에 dto 패키지를 추가하고 dto 패키지에 BoardDTO 클래스를 추가
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BoardDTO {

    private Long bno;
    private String title;
    private String content;
    private String writer;
    private LocalDateTime regDate;
    private LocalDateTime modDate;

}

 


프로젝트에 service 패키지를 추가하고 BoardService 인터페이스와 BoardServiceImpl 클래스를 추가

 

    📍  등록 작업 처리       -  BoardService 인터페이스에 register()를 선언

public interface BoardService {

    Long register(BoardDTO boardDTO);
}

 

    📍  BoardServiceImpl은 ModelMapper와 BoardRepository를 주입 받도록 구현

@Service
@Log4j2
@RequiredArgsConstructor
@Transactional
public class BoardServiceImpl implements BoardService {

    private final ModelMapper modelMapper;

    private final BoardRepository boardRepository;

    @Override
    public Long register(BoardDTO boardDTO) {
        Board board = modelMapper.map(boardDTO, Board.class);
        Long bno = boardRepository.save(board).getBno();
        return bno;
    }
}

 

  ✓  BoardServiceImpl 클래스에는 의존성 주입 외에도 @Transactional 어노테이션을 적용
         ➡️  @Transactional을 적용하면 스프링은 해당 객체를 감싸는 별도의 클래스를 생성해 내는데, 간혹 여러 번의 데이터베이스 연결이 있을 수도 있으므로 트랜잭션 처리는 기본적으로 적용해 두는 것이 좋음

 

    📍  test 폴더에 service 패키지를 추가하고 BoardServiceTests 클래스를 추가

@SpringBootTest
@Log4j2
class BoardServiceImplTest {
    @Autowired
    private BoardService boardService;

    @Test
    public void registerTest() {
        log.info(boardService.getClass().getName());
    }
}

 

  ✓  실제 boardService 변수가 가르키는 객체의 클래스명을 출력하는데 실행해 보면 BoardServiceImpl이 나오지 않고 스프링에서 BoardServiceImpl을 감싸서 만든 클래스 정보가 출력

 

 

    📍  등록처리의 구현은 다음과 같이 작성  -  콘솔에서 insert문이 동작하는지 확인하고 최종적으로 데이터 베이스에서 확인

    @Test
    public void registerTest() {
        log.info(boardService.getClass().getName());

        BoardDTO boardDTO = BoardDTO.builder()
                .title("test...")
                .content("test...")
                .writer("user")
                .build();

        Long bno = boardService.register(boardDTO);
        log.info("bno: " + bno);
    }

 


 

조회 작업 처리


    📍  조회 작업 처리는 특정한 게시물의 번호를 이용하므로 BoardService와 BoardServiceImpl에 코드를 추가

BoardDTO readOne(Long bno);
   @Override
    public BoardDTO readOne(Long bno) {

        Optional<Board> optionalBoard = boardRepository.findById(bno);
        Board board = optionalBoard.orElseThrow();

        return modelMapper.map(board, BoardDTO.class);
    }

 

Optional<Board> optionalBoard = boardRepository.findById(bno);:

boardRepository.findById(bno)는 주어진 bno를 사용하여 데이터베이스에서 Board 객체를 찾음

이 메서드는 Optional<Board>를 반환 (Optional은 값이 존재할 수도 있고, 존재하지 않을 수도 있는 컨테이너)

이를 통해 NullPointerException을 피할 수 있음

 

Board board = optionalBoard.orElseThrow();:

optionalBoard.orElseThrow()는 Optional 객체가 비어있으면 예외를 던지고, 값이 존재하면 그 값을 반환

여기서는 Optional에 Board 객체가 없으면 기본적으로 NoSuchElementException이 발생

이렇게 함으로써, 해당 bno에 해당하는 Board가 없는 경우 예외가 발생하게 됩니다.

 

return modelMapper.map(board, BoardDTO.class);:

modelMapper.map(board, BoardDTO.class)는 Board 객체를 BoardDTO 객체로 변환

ModelMapper는 객체 변환을 쉽게 해주는 라이브러리로, 여기서는 Board 엔티티를 BoardDTO로 변환하는 데 사용

최종적으로 변환된 BoardDTO 객체를 반환

    @Test
    public void readOneTest() {
        log.info(boardService.readOne(100L));
    }

 

수정 작업 처리


    📍  수정 작업은 기존 엔티티 객체에서 필요한 부분만 변경하도록 작성

void modify(BoardDTO boardDTO);
    @Override
    public void modify(BoardDTO boardDTO) {
        Optional<Board> optionalBoard = boardRepository.findById(boardDTO.getBno());
        Board board = optionalBoard.orElseThrow();
        board.change(boardDTO.getTitle(), boardDTO.getContent());
        boardRepository.save(board);
    }

 

    📍  테스트 할 때는 반드시 실제 데이터베이스에 존재하는 번호를 이용해서 확인

    @Test
    public void testModify() {
        BoardDTO boardDTO = BoardDTO.builder()
                .bno(100L)
                .title("update...100")
                .content("update content 100")
                .build();
        boardService.modify(boardDTO);
    }

 


삭제 작업 처리


    📍  삭제 처리에는 게시물의 번호 (bno)만 필요

void remove(Long bno);
    @Override
    public void remove(Long bno) {
        boardRepository.deleteById(bno);
    }

 


 

3)  목록 / 검색 처리

dto 패키지에 PageRequestDTO, PageResponseDTO 클래스 추가

 

PageRequestDTO

 

    📍  페이징 관련 정보 외에 검색의 종류 type와 키워드 keyword를 추가해서 지정

@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {

    @Builder.Default
    private int page = 1;

    @Builder.Default
    private int size = 10;

    private String type; // 검색의 종류 t, c, w, tc, tw, twc

    private String keyword;
}

 

  ✓  검색의 종류는 문자열 하나로 처리해서 나중에 각 문자를 분리하도록 구성

    📍  PageRequestDTO에는 몇 가지 필요한 기능들이 존재

  • 현재 검색 조건들을 BoardRepository에서 String[]로 처리하기 때문에 type이라는 문자열을 배열로 변환해 주는 기능이 필요.
  • 페이징 처리를 위해서 사용하는 Pageable 타입을 반환하는 기능도 필요.
    public String[] getTypes() {
        if (this.type == null || type.isEmpty()) {
            return null;
        }
        return this.type.split("");
    }

    public Pageable getPageable(String...props) {
        return PageRequest.of(this.page - 1, this.size, Sort.by(props).descending());
    }

    private String link;

    public String getLink() {
        if (link == null) {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("page=").append(this.page);
            stringBuilder.append("&size=").append(this.size);

            if (type != null && type.length() > 0) {
                stringBuilder.append("&type=").append(this.type);
            }
            if (keyword != null) {
                stringBuilder.append("&keyword=").append(URLEncoder.encode(this.keyword, StandardCharsets.UTF_8));
            }
            link = stringBuilder.toString();
        }
        return link;
    }

 

  • getTypes(): type 문자열을 분리하여 문자열 배열로 반환합니다. type이 null이거나 비어있으면 빈 배열을 반환
  • getPageable(String... props): 페이지 요청(PageRequest) 객체를 생성. 페이지 번호는 page - 1 (0부터 시작하기 때문), 페이지 크기는 size, 정렬 기준은 주어진 속성(props)을 기준으로 내림차순
  • getLink 메소드는 객체의 page, size, type, keyword 값을 기반으로 URL 쿼리 스트링을 동적으로 생성하고, 한 번 생성된 링크를 캐시하여 이후 호출 시 재사용. 이는 불필요한 문자열 연산을 피하기 위해 유용
  • if (type != null && type.length() > 0): type이 null이 아니고 비어 있지 않은 경우에만 type 파라미터를 추가. 예: &type=tc
  • if (keyword != null): keyword가 null이 아닌 경우에만 keyword 파라미터를 추가. 이때 URLEncoder.encode를 사용하여 keyword를 UTF-8로 인코딩. 이는 URL에 안전하게 포함시키기 위해 필요. 예: &keyword=example

 

PageResponseDTO

 

    📍  화면에 DTO 목록과 시작 페이지 / 끝 페이지 등에 대한 처리를 담당

@Getter
@ToString
public class PageResponseDTO<E> {
    private int page; // 페이지 번호
    private int size; // 페이지 크기
    private int total; // 전체 항목수

    private int startPage; // 시작 페이지 번호
    private int endPage; // 끝 페이지 번호

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

    private List<E> dtoList; // 페이지에 포함된 데이터 목록

    @Builder(builderMethodName = "withAll")
    public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList, int total) {
        if (total <= 0) { // 총 항목 수가 0 이하인 경우, 생성자 실행을 중단
            return;
        }
        this.page = pageRequestDTO.getPage(); // 현재 페이지 번호
        this.size = pageRequestDTO.getSize(); // 한 페이지에 포함된 항목 수
        this.total = total;                   // 전체 항목 수
        this.dtoList = dtoList;               // 데이터 리스트

        this.endPage = (int)(Math.ceil(this.page / 10.0) * 10); // 끝 페이지 번호를 계산
        this.startPage = this.endPage - 9;   // 시작 페이지 번호를 계산
        int last = (int) (Math.ceil(this.total / (double)this.size)); // 실제 마지막 페이지 번호를 계산
        this.endPage = Math.min(endPage, last); // 더 작은 값을 사용
        this.prev = this.startPage > 1; // 시작 페이지 번호가 1보다 큰 경우 이전 페이지가 존재함
        this.next = total > this.endPage * this.size; 
        // 전체 항목 수가 끝 페이지 번호에 페이지 크기를 곱한 값보다 큰 경우 다음 페이지가 존재함

    }
}

 

  ✓  PageResponseDTO 클래스는 페이지네이션 정보와 함께 데이터를 전달하기 위한 데이터 전송 객체

  ✓  public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList, int total): 생성자로, PageRequestDTO, 데이터 리스트(dtoList), 전체 항목 수(total)를 매개변수로 받는다.

 

BoardService / BoardServiceImpl

 

    📍  list()라는 이름으로 목록 / 검색 기능을 선언

PageResponseDTO<BoardDTO> list(PageRequestDTO pageRequestDTO);
    @Override
    public PageResponseDTO<BoardDTO> list(PageRequestDTO pageRequestDTO) {
        String[] types = pageRequestDTO.getTypes(); // 검색 조건을 문자열 배열로 반환
        String keyword = pageRequestDTO.getKeyword(); // 검색 키워드를 반환
        Pageable pageable = pageRequestDTO.getPageable("bno"); // 페이지 요청 객체를 생성

        // 데이터베이스에서 Board 엔티티를 검색하고 페이징 결과를 Page<Board> 객체로 반환
        Page<Board> boardPage = boardRepository.searchAll(types, keyword, pageable);

        // BoardDTO 객체를 저장할 리스트를 생성
        List<BoardDTO> boardDTOList = new ArrayList<>();
        
        // Board 엔티티를 BoardDTO로 변환하여 boardDTOList에 추가
        for (Board board : boardPage.getContent()) {
            // Board 엔티티를 BoardDTO로 변환
            boardDTOList.add(modelMapper.map(board, BoardDTO.class));
        }
        return PageResponseDTO.<BoardDTO>withAll() // PageResponseDTO의 빌더를 생성
                .pageRequestDTO(pageRequestDTO)
                .dtoList(boardDTOList)
                .total((int)boardPage.getTotalElements())
                .build();
    }

 

  ✓  PageRequestDTO 객체를 사용하여 페이지 요청을 처리하고, 결과를 BoardDTO 객체로 변환하여 PageResponseDTO로 반환

 

테스트 코드로 목록 / 검색 기능 확인


    📍  BoardServiceTests에는 테스트 코드를 작성해서 페이징 처리나 쿼리문이 정상적으로 동작하는지 확인

    @Test
    public void testPaging() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .type("tcw")
                .keyword("user")
                .page(1)
                .size(10)
                .build();
        PageResponseDTO<BoardDTO> responseDTO = boardService.list(pageRequestDTO);
        log.info(responseDTO);
        log.info("1) 페이지 번호: " + responseDTO.getPage());
        log.info("2) 전체 게시물 수: " + responseDTO.getTotal());
        log.info("3) 현재 페이지에 출력될 게시물을 반복문을 이용해서 순서대로 출력: ");
        responseDTO.getDtoList().forEach(boardDTO -> {log.info(boardDTO);});
    }

 

 

 

 

 

 

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

+ Recent posts