자바 스프링 부트 4일차 b01

2026. 4. 6. 20:59대우개발원 수업 내용/spring boot, framework

반응형

bulid.gradle에 modelmapper 의존성(Dependency)  추가

implementation("org.modelmapper:modelmapper:3.1.1")

b01안에 config 패키지 추가

config 패키지 안에 RootConfig class도 추가

RootConfig class에

Entity와 DTO 간의 데이터 변환을 돕는 ModelMapper 객체를 스프링 빈으로 등록하는 설정 코드

Getter나 Setter 없이도 private 필드에 접근해 유연하게 값을 매핑하도록 세부 규칙을 지정하는 역할을 하는 아래 구문 추자

package com.example.b01.config;

import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class 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;
    }
}

 

=> DTO를 만들어야 한다

dto 패키지 만들고 BoardDTO클래스 만듬

Board entity를 참조

게시글 데이터를 계층 간에 전달하기 위한 데이터 전송 객체.

데이터베이스의 게시글 정보인 번호, 제목, 내용, 작성자, 등록일, 수정일 값을 가진다.

상단의 어노테이션들은 Getter, Setter, 생성자, 빌더 패턴 등 필수 코드를 자동으로 생성해주는 롬복 라이브러리 기능이다.

package com.example.b01.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
@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 interface 클래스 추가

package com.example.b01.service;

import com.example.dto.BoardDTO;

public interface BoardService {
    Long register(BoardDTO boardDTO);
}

 

BoardServiceImpl 클래스 생성

게시글 등록 등의 비즈니스 로직을 전담하는 서비스(Service) 계층의 구현체 코드

핵심 로직 (register 메서드)

  • DTO → Entity 변환: ModelMapper를 사용해 화면에서 넘어온 BoardDTO 객체를 DB 저장용 Board 엔티티로 변환
  • DB 저장 및 PK 반환: boardRepository.save()로 데이터를 DB에 INSERT하고, 자동 생성된 게시글 번호(bno)를 추출해 반환

주요 애노테이션

  • @Service: 해당 클래스를 스프링 컨테이너에 서비스 빈(Bean)으로 등록
  • @RequiredArgsConstructor: final로 선언된 ModelMapper와 BoardRepository 객체를
    스프링이 자동으로 주입 (생성자 주입 방식)
package com.example.b01.service;

import com.example.b01.domain.Board;
import com.example.b01.repository.BoardRepository;
import com.example.dto.BoardDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;

@Service
@Log4j2
@RequiredArgsConstructor
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;
    }
}

트랜잭션이란?
트랜잭션(Transaction)은 데이터베이스와 관련된 작업을 일관성 있게 처리하기위한 개념.

데이터베이스에서는 여러 개의 작업(예: 데이터 읽기, 쓰기,업데이트)이 수행될 때 데이터의 일관성을 보장하기 위해 

트랜잭션을 사용 . 트랜잭션은 " 원자성 (Atomicity)", " 일관성 (Consistency)","격리성(Isolation)", "지속성(Durability)"을 

나타내는 "ACID"라는 속성들을 가지고 있다.

 

• 원자성 (Atomicity)
트랜잭션은 하나의 작업 단위로 간주되어, 모든 작업이 성공하면 커밋(Commit)되고, 하나라도 실패하면 롤백(Rollback)되어

이전 상태로 복구. 즉, 트랜잭션 내의 모든 작업은 원자적으로 실행
• 일관성 (Consistency)
트랜잭션은 데이터베이스 상태를 일관된 상태로 유지.

트랜잭션이 시작하기 전과 끝난 후에도 데이터베이스는 일관성을 유지해야 함
• 격리성 (Isolation)
여러 개의 트랜잭션이 동시에 실행될 때, 각 트랜잭션은 다른 트랜잭션의 작업에 영향을 받지 않아야 함
격리성은 동시성 문제를 해결하는 데 중요한 역할을 합니다.
• 지속성 (Durability)
트랜잭션이 성공적으로 커밋되면 그 결과는 영구적으로 저장되어야 함.

시스템이 다운되더라도 데이터는 손실되지 않아야 함.


BoardServiceImpl에 안정성을 위해

@Transactional

을 추가해준다.


Test 패지키 안에

service클래스 추가

BoardServiceTests도 추가

아래 구문을 추가하고

package com.example.b01.service;

import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@Log4j2
public class BoardServiceTests {
    @Autowired
    private BoardService boardService;

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

 

실행 하면

log.info(boardService.getClass().getName());

이 부분에 대해 나오는게 아래 결과이다

 

왜 이름 뒤에 $$SpringCGLIB$$0가 붙어서 나오는지?

스프링(Spring Boot)은 @Autowired로 서비스 객체를 주입할 때, 우리가 작성한 BoardServiceImpl 원본 객체를 그대로 주입하지 않습니다. 대신, 그 원본 객체를 감싸는 프록시(Proxy) 가짜 객체를 동적으로 생성하여 주입합니다.

  • 이유: 메서드 실행 전후로 트랜잭션(@Transactional) 처리나 로깅 등 부가적인 기능(AOP)을 스프링이 알아서 가로채서 처리해주기 위함입니다.
  • 결론: 이름에 $$SpringCGLIB$$가 붙어 있다는 것은, 해당 객체가 스프링이 CGLIB라는 라이브러리를 통해 만들어낸 프록시 객체이며 의존성 주입(DI)이 아주 정상적으로 잘 이루어졌음을 의미합니다.

BoardServiceTests 구문을 더 수정

 

BoardService의 register(게시글 등록) 기능 정상 작동 확인을 위한 테스트 코드

코드 동작 순서 분석
1. 가짜 데이터 생성 (BoardDTO.builder)
- 화면에서 제목, 내용, 작성자를 입력해 '등록' 버튼을 누른 상황을 가정하여 DTO 객체 생성
2. 서비스 로직 실행 (boardService.register)
- 생성된 DTO를 서비스 계층으로 전달
- ModelMapper를 이용해 DTO를 Entity로 변환 후 DB 저장(INSERT) 수행
3. 결과 확인 (log.info)
- 저장 완료 후 자동 생성된 게시글 번호(PK)를 리턴받아 콘솔 로그로 출력

 

요약
- 화면 데이터 유입 가정 하에 서비스 계층을 통한 DB 저장 및 PK 반환 흐름을 검증하는 테스트임

@Test
public void testRegister() {
    log.info(boardService.getClass().getName());
    BoardDTO boardDTO = BoardDTO.builder()
            .title("Sample Title.......")
            .content("Sample Content........")
            .writer("user00")
            .build();
    Long bno = boardService.register(boardDTO);
    log.info("bno: " + bno);
}
더보기
더보기

전체코드

package com.example.b01.service;

import com.example.dto.BoardDTO;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@Log4j2
public class BoardServiceTests {
    @Autowired
    private BoardService boardService;

    @Test
    public void testRegister() {
        log.info(boardService.getClass().getName());
        BoardDTO boardDTO = BoardDTO.builder()
                .title("Sample Title.......")
                .content("Sample Content........")
                .writer("user00")
                .build();
        Long bno = boardService.register(boardDTO);
        log.info("bno: " + bno);
    }
}

 

실행하면

아래 사진에서 테이블이 한개 추가 된게 보임


BoardService.java에 아래 구문 추가

BoardDTO readOne(Long bno);

BoardServiceImpl에 아래 구문 추가

@Override
public BoardDTO readOne(Long bno) {
    Optional<Board> result = boardRepository.findById(bno);
    Board board = result.orElseThrow();
    BoardDTO boardDTO = modelMapper.map(board, BoardDTO.class);
    return boardDTO;
}

 


BoardService.java에 아래 구문 추가

void modify(BoardDTO boardDTO);

BoardServiceImpl에 아래 구문 추가

//modify 메서드 선언
@Override
public void modify(BoardDTO boardDTO) {
    Optional<Board> result = boardRepository.findById(boardDTO.getBno());
    Board board = result.orElseThrow();
    board.change(boardDTO.getTitle(), boardDTO.getContent());
    boardRepository.save(board);
}

BoardServiceTests에 아래 구문을 추가해서

    @Test
    public void testModify() {
        // 변경이 필요한 데이터만
        BoardDTO boardDTO = BoardDTO.builder()
                .bno(202L)
                .title("Update content 202....")
                .content("Update content 202......")
                .build();
        boardService.modify(boardDTO);
    }

실행하면

더보기
더보기

 

  • BUILD SUCCESSFUL in 8s: 에러 없이 빌드와 테스트가 모두 정상 완료되었다는 뜻
  • Hibernate: 로그:
    • select ... where bno=?: 해당 번호의 게시글을 잘 찾아왔고
    • update board set content=?, moddate=?, title=? ...: content 데이터까지 제대로 들어가서 수정(update) 쿼리가 DB로 정상적으로 날아갔음을 확인함.

 


BoardService.java에 아래 구문 추가

void remove(Long bno);

BoardServiceImpl에 아래 구문 추가

 

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

 

지금까지는 한개만 찾아서 동작하게 하는거였으니 이제

전체를 조회 가능한것을 구현


PageRequestDTO 생성

스프링 부트 환경에서 게시판 목록을 조회할 때, 클라이언트로부터 전달받는 페이징 및 검색 정보를 담는 데이터 전송 객체

 

  1. 변수 선언부 (데이터 저장) page 변수는 현재 페이지 번호를 의미하며 기본값은 1.
    size 변수는 한 화면에 출력할 게시물의 개수를 의미하며 기본값은 10.
    type 변수는 제목, 내용, 작성자 등 어떤 조건으로 검색할지를 나타내는 문자열.
    keyword 변수는 사용자가 입력한 실제 검색어 문자열을 저장.
  2. getTypes 메서드 (검색 조건 분리) 클라이언트가 제목과 내용을 동시에 검색하려고 tc라는 문자열을 보내면,
    데이터베이스 쿼리를 동적으로 생성할 때 반복문을 돌리기 쉽도록 이를 t와 c로 분리된 배열 형태로 변환하여 반환.
  3. getPageable 메서드 (JPA 페이징 객체 변환) 스프링 데이터 JPA가 이해할 수 있는 페이징 전용 객체를 생성하여 반환.
    화면에서는 페이지 번호가 1부터 시작하지만 데이터베이스 조회 시에는 인덱스가 0부터 시작하므로
    page 변수 값에서 1을 뺀 값으로 설정. 추가로 파라미터로 넘겨받은 속성을 기준으로 데이터를 내림차순 정렬.
  4. getLink 메서드 (URL 상태 유지) 게시물을 조회하거나 수정한 뒤 다시 목록으로 돌아올 때 기존의 페이지 번호와
    검색 상태를 유지하기 위한 URL 파라미터 문자열을 조립.
    생성된 문자열은 link 변수에 저장해 두었다가 재사용하여 성능을 높이며, 검색어의 한글 깨짐을 방지하기 위해
    UTF-8 인코딩 처리를 함께 수행.
더보기
더보기
package com.example.b01.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

@Data
@Builder
@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;

    public String[] getTypes(){
        if(type == null || type.isEmpty()){
            return null;
        }
        return 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 builder = new StringBuilder();
            builder.append("page=" + this.page);
            builder.append("&size=" + this.size);
            if(type != null && type.length() > 0){
                builder.append("&type=" + type);
            }
            if(keyword != null){
                try {
                    builder.append("&keyword=" + URLEncoder.encode(keyword,"UTF-8"));
                } catch (UnsupportedEncodingException e) {
                }
            }
            link = builder.toString();
        }
        return link;
    }


}
 

 


PageResponseDTO 클래스 추가

조회된 데이터와 페이징 계산 결과를 함께 전달하는 응답 전용 객체.

1. 데이터 구성

  • dtoList: 실제 조회된 게시글 목록 데이터.
  • page/size/total: 현재 페이지, 한 페이지당 개수, 전체 데이터 총 개수.
  • start/end: 화면 하단에 표시할 페이지 번호의 시작과 끝.
  • prev/next: 이전/다음 구간 이동 버튼 활성화 여부.

2. 주요 계산 로직

  • end 계산: 현재 페이지를 기준으로 10 단위의 끝 번호를 임시 계산함.
  • start 계산: 끝 번호에서 9를 빼서 시작 번호를 구함.
  • 끝 번호 보정: 전체 데이터 개수(total)를 기준으로 계산한 실제 마지막 페이지가 임시 끝 번호보다 작으면 그 값을 실제 끝 번호로 바꿈.
  • 버튼 활성화: 시작 번호가 1보다 크면 prev 활성화, 전체 개수가 현재 끝 번호 구간보다 많으면 next 활성화.
더보기
더보기
package com.example.b01.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

import java.util.List;

@Getter
@ToString
public class PageResponseDTO<E> {

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

    //시작 페이지 번호
    private int start;
    //끝 페이지 번호
    private int end;
    private boolean prev;
    //다음 페이지의 존재 여부
    private boolean next;

    private List<E> dtoList;

    @Builder(builderMethodName = "withAll")
    public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList, int total){
        if(total <= 0){
            return;
        }
        this.page = pageRequestDTO.getPage();
        this.size = pageRequestDTO.getSize();
        this.total = total;
        this.dtoList = dtoList;
        this.end =   (int)(Math.ceil(this.page / 10.0 )) *  10;//화면에서의 마지막 번호
        this.start = this.end - 9;//화면에서의 시작 번호
        int last =  (int)(Math.ceil((total/(double)size)));//데이터의 개수를 계산한 마지막  페이지 번호
        this.end =  end > last ? last: end;
        this.prev = this.start > 1;
        this.next =  total > this.end * this.size;
    }

}

 

 

BoardService에 아래 구문 추가

 

게시물 목록 조회를 담당하는 서비스 메서드의 규칙(인터페이스 선언)

1. 입력 (파라미터)
PageRequestDTO: 화면에서 전달한 요청 정보(볼 페이지 번호, 검색 조건, 검색어)를 넘겨받습니다.

2. 출력 (리턴 값)
PageResponseDTO<BoardDTO>: 요청 조건에 맞춰 DB에서 조회한 게시글 데이터 목록과 화면 하단에 그릴 페이징 번호 계산 결과를 통째로 담아서 반환합니다.메서드 선언

PageResponseDTO<BoardDTO> list(PageRequestDTO pageRequestDTO);

 


BoardServiceImpl.java 코드에 아래 구문 추가

 

게시글 목록과 페이징 정보를 DB에서 조회해 화면으로 반환하는 실제 동작 로직입니다.

1. 요청 분해 전달받은 pageRequestDTO에서 검색 조건(types), 검색어(keyword), DB 페이징 객체(pageable)를 꺼냅니다.

2. DB 조회 boardRepository.searchAll을 실행해 실제 데이터베이스에서 조건에 맞는 게시글(Board 엔티티)을 검색하고 페이징하여 가져옵니다.

3. DTO 변환 가져온 DB 전용 엔티티(Board) 목록을 ModelMapper를 사용해 화면 전달용 객체인 BoardDTO 리스트로 일괄 변환합니다.

4. 결과 조립 및 반환 최종 응답 객체인 PageResponseDTO를 생성합니다. 여기에 화면이 보냈던 요청 정보, 변환이 끝난 게시글 리스트(dtoList), DB에 있는 전체 데이터 개수(total)를 담아서 반환합니다.

 

@Override
public PageResponseDTO<BoardDTO> list(PageRequestDTO pageRequestDTO) {
    String[] types = pageRequestDTO.getTypes();
    String keyword = pageRequestDTO.getKeyword();
    Pageable pageable = pageRequestDTO.getPageable("bno");

    Page<Board> result = boardRepository.searchAll(types, keyword, pageable);

    List<BoardDTO> dtoList = result.getContent().stream()
            .map(board -> modelMapper.map(board,BoardDTO.class))
            .collect(Collectors.toList());

    return PageResponseDTO.<BoardDTO>withAll()
            .pageRequestDTO(pageRequestDTO)
            .dtoList(dtoList)
            .total((int)result.getTotalElements())
            .build();
}

정상적인 코드인지 확인하는 테스트 코드

BoardServiceTests 아래 코드 추가

 

제목+내용+작성자에 '1'이 들어간 게시물을 1페이지에 10개씩 가져오는 기능이 제대로 동작하는가? 테스트하는 코드

@Test
public void testList() {
    PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
            .type("tcw")
            .keyword("1")
            .page(1)
            .size(10)
            .build();
    PageResponseDTO<BoardDTO> responseDTO
            =boardService.list(pageRequestDTO);
    log.info(responseDTO);
}

BoardController 클래스 추가

 

컨트롤러에서는 서비스를 사용하므로

서비스 객체 추가

package com.example.b01.controller;


import com.example.b01.dto.PageRequestDTO;
import com.example.b01.service.BoardService;
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;

@Controller
@RequestMapping("/board")
@Log4j2
@RequiredArgsConstructor

public class BoardController {
    private final BoardService boardService;

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

    }
}

 

Simple Sidebar 부트스트랩 다운

압축 풀어서

사진과 같이 파일 옮김

 

정상적으로 작동되는 모습

 

basic.html 파일을 만듬

 

index.html의 내용을 basic.html 안에 넣음


basic.html

사진의 부분을 변경

    <!-- Favicon -->
    <link rel="icon" type="image/x-icon" th:href="@{/assets/favicon.ico}" />

    <!-- Core theme CSS (includes Bootstrap) -->
    <link rel="stylesheet" th:href="@{/css/styles.css}" />

layout:fragment="content"

<!-- Core theme JS -->
<script th:src="@{/js/scripts.js}"></script>

<th:block layout:fragment="script">

board 디렉토리 추가

list.html

추가

<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        xmlns:th="http://www.thymeleaf.org"
        layout:decorate="~{layout/basic.html}">
<div layout:fragment="contnet">
    <h1>Board List</h1>
</div>

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

 

실행하면 정상적으로 script가 console창에 뜨는 모습


BoardController에

    PageResponseDTO<BoardDTO> responseDTO = boardService.list(pageRequestDTO);
    log.info(responseDTO);
    model.addAttribute("responseDTO", responseDTO);
}

추가


list.html 코드에 아래 구문 추가

더보기
더보기
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/basic.html}">

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


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

 

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

실행하

 

list.html을 아래 구문으로 업데이트

더보기
더보기
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/basic.html}">

<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
            </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.dtoList}"  >
                     <th scope="row">[[${dto.bno}]]</th>
                     <td>
                        [[${dto.title}]]
                     </td>
                     <td>[[${dto.writer}]]</td>
<!--                     <td>[[${dto.regDate}]]</td>-->
                     <td>[[${#temporals.format(dto.regDate, 'yyyy-MM-dd')}]]</td>
                  </tr>
                  </tbody>
               </table>
               <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>

            </div><!--end card body-->
         </div><!--end card-->
      </div><!-- end col-->
   </div><!-- end row-->

</div>


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

 

링크 처리 전이지만 링크에 페이지5를 입력하면 정상적으로 페이지가 이동하는 모습

 

상단에 검색창 추가

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

http://localhost:8080/board/list?type=t&keyword=1

아직 링크 처리 전이라 주소를 입력해야 나옴

1. 페이지 이동 (페이징)

  • 동작: 페이지 번호를 클릭하면 기존 검색 조건에 클릭한 페이지 번호를 합쳐서 서버에 보냅니다.
  • 이유: 검색 결과 내에서 다른 페이지로 이동해도 검색어가 풀리지 않게 하기 위해서입니다.

2. 검색 초기화 (Clear)

  • 동작: 초기화 버튼을 누르면 모든 조건을 지우고 첫 목록 페이지로 이동합니다.
  • 이유: 입력한 검색 내용을 싹 지우고 처음부터 다시 보고 싶을 때 사용합니다.

결론: 검색어 유지하며 페이지 넘기기 + 검색 취소 기능입니다.

<script layout:fragment="script" th:inline="javascript">
    console.log("script......")
    document.querySelector(".pagination").addEventListener("click", function (e) {
       e.preventDefault()
       e.stopPropagation()
       const target = e.target
       if(target.tagName !== 'A') {
          return
       }
       const num = target.getAttribute("data-num")
       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)

위의 코드를 추가