Java/JPA

JPA 기초(search) - AWS 풀스택 과정 86일차

awspspgh 2024. 11. 27. 09:39
목차
1. search

 

1. search

build.gradle (boot_JPA)

plugins {
	id 'java'
	id 'war'
	id 'org.springframework.boot' version '3.2.11'
	id 'io.spring.dependency-management' version '1.1.6'
}

group = 'com.ezen'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
	implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
	implementation 'org.apache.tika:tika-core:2.4.1'
	implementation 'org.apache.tika:tika-parsers:2.4.1'
	implementation 'net.coobird:thumbnailator:0.4.17'
	implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
	annotationProcessor 'org.projectlombok:lombok'
	annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
	annotationProcessor "jakarta.annotation:jakarta.annotation-api"
	annotationProcessor "jakarta.persistence:jakarta.persistence-api"
	/*providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'*/
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

def querydslSrcDir='src/main/generated'
clean{
	delete file(querydslSrcDir)
}
tasks.withType(JavaCompile){
	options.generatedSourceOutputDirectory = file(querydslSrcDir)
}
tasks.named('test') {
	useJUnitPlatform()
}

 

 list.html

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

<div layout:fragment="content">
    <div class="container-md">
        <h1>Board List Page</h1>
        <!-- search line -->
        <!--[[${pgvo}]]-->
        <div class="container-fluid">
            <form action="/board/list" method="get" class="d-flex" role="search">
                <select class="form-select" name="type" id="inputGroupSelect01">
                    <option th:selected="${pgvo.type == null ? 'true' : 'false' }">Choose...</option>
                    <option th:value="t" th:selected="${pgvo.type == 't' ? 'true' : 'false' }">title</option>
                    <option th:value="w" th:selected="${pgvo.type == 'w' ? 'true' : 'false' }">writer</option>
                    <option th:value="c" th:selected="${pgvo.type == 'c' ? 'true' : 'false' }">content</option>
                    <option th:value="tw" th:selected="${pgvo.type == 'tw' ? 'true' : 'false' }">title + writer</option>
                    <option th:value="wc" th:selected="${pgvo.type == 'wc' ? 'true' : 'false' }">writer + content</option>
                    <option th:value="tc" th:selected="${pgvo.type == 'tc' ? 'true' : 'false' }">content + title</option>
                    <option th:value="twc" th:selected="${pgvo.type == 'twc' ? 'true' : 'false' }">all</option>
                </select>
                <input class="form-control me-2" name="keyword" type="text" placeholder="Search" th:value="${pgvo.keyword}" aria-label="Search">
                <input type="hidden" name="pageNo" th:value="1">
<!--                <input type="hidden" name="qty" th:value="${pgvo.qty }">-->
                <button type="submit" class="btn btn-primary position-relative">
                    search
                    <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
                        [[${list.totalElements}]]
		            </span>
                </button>
            </form>
        </div>
        <table class="table table-hover">
            <thead>
            <tr>
                <th>#</th>
                <th>title</th>
                <th>writer</th>
                <th>regDate</th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="bvo:${list}">
                <td>[[${bvo.bno}]]</td>
                <td><a th:href="@{/board/detail(bno=${bvo.bno})}">[[${bvo.title}]]</a></td>
                <td>[[${bvo.writer}]]</td>
                <td>[[${bvo.regAt}]]</td>
            </tr>
            </tbody>
        </table>

        <!-- 페이지네이션 라인 -->
        <!-- th:classappend : 동적 클래스 추가 -->

        <nav aria-label="Page navigation example">
            <ul class="pagination justify-content-center">
                <li th:if="${pgvo.hasPrev}" class="page-item">
                    <a class="page-link" th:href="@{/board/list(pageNo = ${pgvo.startPage - 1}, type=${pgvo.type}, keyword=${pgvo.keyword})}" aria-label="Previous">
                        <span aria-hidden="true">&laquo;</span>
                    </a>
                </li>

                <th:block th:each="i : ${#numbers.sequence(pgvo.startPage, pgvo.endPage)}">
                    <li class="page-item" aria-current="page" th:classappend="${pgvo.pageNo eq i ? 'active' : ''}">
                        <a class="page-link" th:href="@{/board/list(pageNo = ${i}, type=${pgvo.type}, keyword=${pgvo.keyword})}">[[${i}]]</a>
                    </li>
                </th:block>

                <li th:if="${pgvo.hasNext}" class="page-item">
                    <a class="page-link" th:href="@{/board/list(pageNo = ${pgvo.endPage + 1}, type=${pgvo.type}, keyword=${pgvo.keyword})}" aria-label="Next">
                        <span aria-hidden="true">&raquo;</span>
                    </a>
                </li>
            </ul>
        </nav>
    </div>
</div>

 

 PagingVO.java

package com.ezen.boot_JPA.dto;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.domain.Page;

@Getter
@Setter
@ToString
public class PagingVO {
    private int totalPage;
    private int startPage;
    private int endPage;
    private boolean hasPrev, hasNext;
    private int pageNo;

    private String type;
    private String keyword;

    public PagingVO(Page<BoardDTO> list, int pageNo, String type, String keyword){
        this.pageNo = pageNo + 1;
        this.totalPage = list.getTotalPages();
        this.type = type;
        this.keyword = keyword;

        this.endPage = (int)Math.ceil(this.pageNo / 10.0) * 10;
        this.startPage = endPage - 9;
        if(endPage > totalPage){
            endPage = totalPage;
        }
        this.hasPrev = this.startPage > 10;
        this.hasNext = this.endPage < this.totalPage;
    }
}

 

 BoardController.java

package com.ezen.boot_JPA.controller;

import com.ezen.boot_JPA.dto.BoardDTO;
import com.ezen.boot_JPA.dto.BoardFileDTO;
import com.ezen.boot_JPA.dto.FileDTO;
import com.ezen.boot_JPA.dto.PagingVO;
import com.ezen.boot_JPA.handler.FileHandler;
import com.ezen.boot_JPA.handler.FileRemoveHandler;
import com.ezen.boot_JPA.service.BoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.util.List;

@Slf4j
@RequestMapping("/board/*")
@RequiredArgsConstructor
@Controller
public class BoardController {
    private final BoardService boardService;
    private final FileHandler fileHandler;

    @GetMapping("/register")
    public void register(){}

/*    @PostMapping("/register")
    public String register(BoardDTO boardDTO){
        log.info(">>> boardDTO >>> {}", boardDTO);
        // insert, update, delete => return 1 row
        // jpa insert, update, delete => return id
        Long bno = boardService.insert(boardDTO);
        log.info(">>> insert >>> {}", bno > 0 ? "OK" : "FAIL");
        return "/index";
    }*/

    @PostMapping("/register")
    public String Register(BoardDTO boardDTO, @RequestParam(name = "files", required = false)
                           MultipartFile[] files){
        List<FileDTO> flist = null;
        if(files != null && files[0].getSize() > 0){
            // 파일 핸들러 작업
            flist = fileHandler.uploadFiles(files);
        }
        long bno = boardService.insert(new BoardFileDTO(boardDTO, flist));
        return "/index";
    }

    /*@GetMapping("/list")
    public void list(Model model){
        // paging이 없는 케이스
        List<BoardDTO> list = boardService.getList();
        model.addAttribute("list", list);
    }*/

/*    @GetMapping("/list")
    public void list(Model model, @RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo){
        // 화면에서 들어오는 pageNo = 1 / 0으로 처리가 되어야 함
        // 화면에서 들어오는 pageNo = 2 / 1로 처리가 되어야 함
        log.info(">>> pageNo >>> {}", pageNo);
        pageNo = (pageNo == 0 ? 0 : pageNo - 1);
        log.info(">>> pageNo >>> {}", pageNo);
        Page<BoardDTO> list = boardService.getList(pageNo);

        log.info(">>> list >>> {}", list.toString());
        log.info(">>> totalCount >>> {}", list.getTotalElements()); // 전체 글 수
        log.info(">>> totalPage >>> {}", list.getTotalPages()); // 전체 페이지 수 => realEndPage
        log.info(">>> pageNumber >>> {}", list.getNumber()); // 전체 페이지 번호 => pageNo
        log.info(">>> pageSize >>> {}", list.getSize()); // 한 페이지에 표시되는 길이 => qty
        log.info(">>> next >>> {}", list.hasNext()); // next 여부
        log.info(">>> prev >>> {}", list.hasPrevious()); // prev 여부

        PagingVO pgvo = new PagingVO(list, pageNo);
        log.info(">>> pgvo >>> {}", pgvo.toString());
        model.addAttribute("list", list);
        model.addAttribute("pgvo", pgvo);
    }*/

    @GetMapping("/list")
    public void list(Model model, @RequestParam(value = "pageNo", defaultValue = "0", required = false) int pageNo,
                     @RequestParam(value = "type", required = false) String type,
                     @RequestParam(value = "keyword", required = false) String keyword) {
        pageNo = (pageNo == 0 ? 0 : pageNo - 1);
        Page<BoardDTO> list = boardService.getList(pageNo, type, keyword); // type, keyword 추가하여 서비스임플로 보내기
        PagingVO pgvo = new PagingVO(list, pageNo, type, keyword);
        model.addAttribute("list", list);
        model.addAttribute("pgvo", pgvo);
    }

    @GetMapping("/detail")
    public void modify(Model model, @RequestParam("bno") Long bno){
//        BoardDTO boardDTO = boardService.getDetail(bno);
        BoardFileDTO boardFileDTO = boardService.getDetail(bno);
        model.addAttribute("boardFileDTO", boardFileDTO);
    }

/*    @PostMapping("/modify")
    public String modify(BoardDTO boardDTO, RedirectAttributes redirectAttributes){
        Long bno = boardService.modify(boardDTO);
        redirectAttributes.addAttribute("bno", boardDTO.getBno());
        return "redirect:/board/detail";
    }   */

    @PostMapping("/modify")
    public String modify(BoardDTO boardDTO, @RequestParam(name = "files", required = false) MultipartFile[] files, RedirectAttributes redirectAttributes){
        List<FileDTO> flist = null;
        if(files != null && files[0].getSize() > 0){
            flist = fileHandler.uploadFiles(files);
        }
        Long bno = boardService.modify(new BoardFileDTO(boardDTO, flist));
        redirectAttributes.addAttribute("bno", boardDTO.getBno());
        return "redirect:/board/detail";
    }

    @GetMapping("/delete")
    public String delete(@RequestParam("bno") Long bno){
        boardService.delete(bno);
        return "redirect:/board/list";
    }

    @ResponseBody
    @DeleteMapping("/file/{uuid}")
    public String fileRemove(@PathVariable("uuid") String uuid){
        FileDTO fvo = boardService.getFile(uuid);
        long bno = boardService.fileRemove(uuid);
        FileRemoveHandler fr = new FileRemoveHandler();
        boolean isDel = fr.deleteFile(fvo);
        return (bno > 0 && isDel) ? "1" : "0";
    }
}

 

 BoardService.java

package com.ezen.boot_JPA.service;

import com.ezen.boot_JPA.dto.BoardDTO;
import com.ezen.boot_JPA.dto.BoardFileDTO;
import com.ezen.boot_JPA.dto.FileDTO;
import com.ezen.boot_JPA.entity.Board;
import com.ezen.boot_JPA.entity.File;
import org.springframework.data.domain.Page;

import java.util.List;

public interface BoardService {
    // 추상 메서드만 가능한 인터페이스
    // 메서드가 default(접근제한자) 구현 가능
    Long insert(BoardDTO boardDTO);
    long insert(BoardFileDTO boardFileDTO);

    // BoardDTO(class): bno title writer content regAt modAt
    // Board(table) : bno title writer content
    // BoardDTO => board 변환
    // 화면에서 가져온 BoardDTO 객체를 저장을 위한 Board 객체로 변환
    default Board convertDtoToEntity(BoardDTO boardDTO){
        return Board.builder()
                .bno(boardDTO.getBno())
                .title(boardDTO.getTitle())
                .writer(boardDTO.getWriter())
                .content(boardDTO.getContent())
                .build();
    }

    // board => BoardDTO 변환
    // DB에서 가져온 Board 객체를 화면에 뿌리기 위한 BoardDTO 객체로 변환
    default BoardDTO convertEntityToDto(Board board){
        return BoardDTO.builder()
                .bno(board.getBno())
                .title(board.getTitle())
                .writer(board.getWriter())
                .content(board.getContent())
                .regAt(board.getRegAt())
                .modAt(board.getModAt())
                .build();
    }

    // File 객체 convert
    // FileDTO => File Entity
    default File convertDtoToEntity(FileDTO fileDTO){
        return File.builder()
                .uuid(fileDTO.getUuid())
                .saveDir(fileDTO.getSaveDir())
                .fileName(fileDTO.getFileName())
                .fileType(fileDTO.getFileType())
                .bno(fileDTO.getBno())
                .fileSize(fileDTO.getFileSize())
                .build();
    }

    // File Entity => FileDTO
    default FileDTO convertEntityToDto(File file){
        return FileDTO.builder()
                .uuid(file.getUuid())
                .saveDir(file.getSaveDir())
                .fileName(file.getFileName())
                .fileType(file.getFileType())
                .bno(file.getBno())
                .fileSize(file.getFileSize())
                .regAt(file.getRegAt())
                .modAt(file.getModAt())
                .build();
    }

//  List<BoardDTO> getList();

    Page<BoardDTO> getList(int pageNo, String type, String keyword);

//    BoardDTO getDetail(Long bno);

    BoardFileDTO getDetail(Long bno);

//    Long modify(BoardDTO boardDTO);
    Long modify(BoardFileDTO boardFileDTO);

    void delete(Long bno);

    long fileRemove(String uuid);

    FileDTO getFile(String uuid);
}

 

 BoardServiceImpl.java

package com.ezen.boot_JPA.service;

import com.ezen.boot_JPA.dto.BoardDTO;
import com.ezen.boot_JPA.dto.BoardFileDTO;
import com.ezen.boot_JPA.dto.FileDTO;
import com.ezen.boot_JPA.entity.Board;
import com.ezen.boot_JPA.entity.File;
import com.ezen.boot_JPA.repository.BoardRepository;
import com.ezen.boot_JPA.repository.FileRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Slf4j
@RequiredArgsConstructor
@Service
public class BoardServiceImpl implements BoardService{
    private final BoardRepository boardRepository;
    private final FileRepository fileRepository;

    @Override
    public Long insert(BoardDTO boardDTO) {
        // 저장 객체는 Board
        // save() : insert 후 저장 객체의 id를 리턴
        // save() Entity 객체를 파라미터로 전송
        return boardRepository.save(convertDtoToEntity(boardDTO)).getBno();
    }

    @Transactional
    @Override
    public long insert(BoardFileDTO boardFileDTO) {
//        long bno = boardRepository.save(convertDtoToEntity(boardFileDTO.boardDTO())).getBno();
        long bno = insert(boardFileDTO.getBoardDTO());
        if (bno > 0 && boardFileDTO.getFileDTOList() != null){
            for(FileDTO fileDTO : boardFileDTO.getFileDTOList()){
                fileDTO.setBno(bno);
                bno = fileRepository.save(convertDtoToEntity(fileDTO)).getBno();
            }
        }
        return bno;
    }

//    @Override
//    public List<BoardDTO> getList() {
//        // 컨트롤러로 보내야 하는 리턴은 List<BoardDTO>
//        // DB에서 가져오는 리턴은 List<Board> > BoardDTO 객체로 변환
//        // findAll()
//        // 정렬 : Sort.by(Sort.Direction.DESC, "정렬기준 칼럼명")
//        List<Board> boardList = boardRepository.findAll(Sort.by(Sort.Direction.DESC, "bno"));
//        /* List<BoardDTO> boardDTOList = new ArrayList<>();
//        for(Board board : boardList){
//            boardDTOList.add(convertEntityToDto(board));
//        }*/
//        List<BoardDTO> boardDTOList = boardList.stream()
//                .map(b -> convertEntityToDto(b)).toList();
//        return boardDTOList;
//    }


    @Override
    public Page<BoardDTO> getList(int pageNo, String type, String keyword) {
        // pageNo = 0부터 시작
        // 0 => limit 0, 10 / 1 => limit 10, 10
        Pageable pageable = PageRequest.of(pageNo, 10,
                Sort.by("bno").descending());
//        Page<Board> list = boardRepository.findAll(pageable);
        // type, keyword, pageable 값을 주고, Page<Board> list 리턴 받는 메서드 생성
        Page<Board> list = boardRepository.searchBoard(type, keyword, pageable);

        Page<BoardDTO> boardDTOList = list.map(b -> convertEntityToDto(b));
        return boardDTOList;
    }

    @Override
    public BoardFileDTO getDetail(Long bno) {
        Optional<Board> optional = boardRepository.findById(bno);
        if(optional.isPresent()){
            BoardDTO boardDTO = convertEntityToDto(optional.get());

        // file bno에 일치하는 모든 파일 리스트를 가져오기
        // select * from file where bno = #{bno}
            List<File> flist = fileRepository.findByBno(bno);
            List<FileDTO> fileDTOList = flist.stream()
                    .map(f -> convertEntityToDto(f)).toList();
            BoardFileDTO boardFileDTO = new BoardFileDTO(boardDTO, fileDTOList);
            log.info(">>> boardFileDTO >>> {}", boardFileDTO);
            return boardFileDTO;
        }
        return null;
    }

    @Override
    public Long modify(BoardFileDTO boardFileDTO) {
            long bno = insert(boardFileDTO);
            return bno;
        /*Optional<Board> optional = boardRepository.findById(boardFileDTO.getBoardDTO().getBno());
        if(optional.isPresent()){
            Board board = optional.get();
            board.setTitle(boardFileDTO.getBoardDTO().getTitle());
            board.setContent(boardFileDTO.getBoardDTO().getContent());
            long bno = boardRepository.save(board).getBno();
            if (bno > 0 && boardFileDTO.getFileDTOList() != null) {
                for (FileDTO fileDTO : boardFileDTO.getFileDTOList()) {
                    fileDTO.setBno(bno);
                    bno = fileRepository.save(convertDtoToEntity(fileDTO)).getBno();
                }
                return bno;
            }
        }
        return 0L;*/
    }

   /* @Override
    public BoardDTO getDetail(Long bno) {
        *//* findById : 아이디(PK)를 주고 해당 객체를 리턴
            findById의 리턴타입 Optional<Board> 타입으로 리턴
            Optional<T> : nullPointException이 발생하지 않도록 도와줌.
            Optional.isEmpty() : null일 경우 확인 가능 true / false
            Optional.isPresent() : 값이 있는지 확인 true / false
            Optional.get() : 객체 가져오기
        * *//*
        Optional<Board> optional = boardRepository.findById(bno);
        if(optional.isPresent()){
            BoardDTO boardDTO = convertEntityToDto(optional.get());
            return boardDTO;
        }
        return null;
    }*/

//    @Override
//    public Long modify(BoardDTO boardDTO) {
//        // update : jpa는 업데이트가 없음.
//        // 기존 객체를 가져와서 set 수정 후 다시 저장
//        Optional<Board> optional = boardRepository.findById(boardDTO.getBno());
//        if(optional.isPresent()){
//            Board entity = optional.get();
//            entity.setTitle(boardDTO.getTitle());
//            entity.setContent(boardDTO.getContent());
//            // 다시 저장 (기존 객체에 덮어쓰기)
//            return boardRepository.save(entity).getBno();
//        }
//        return null;
//    }

    // 삭제 : deleteById(id)

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

    @Override
    public long fileRemove(String uuid) {
        Optional<File> optional = fileRepository.findById(uuid);
        if(optional.isPresent()){
            fileRepository.deleteById(uuid);
            return optional.get().getBno();
        }
        return 0;
    }

    @Override
    public FileDTO getFile(String uuid) {
        Optional<File> optional = fileRepository.findById(uuid);
        log.info(">>> op >>> {}", optional);
        if(optional.isPresent()){
            FileDTO fileDTO = convertEntityToDto(optional.get());
            return fileDTO;
        }

        return null;
    }

}

 

 BoardRepository.java

package com.ezen.boot_JPA.repository;

import com.ezen.boot_JPA.entity.Board;
import org.springframework.data.jpa.repository.JpaRepository;

/* JpaRepository<테이블, id> */
public interface BoardRepository extends JpaRepository<Board, Long>, BoardCustomRepository {
}

 

 BoardCustomRepository.java

package com.ezen.boot_JPA.repository;

import com.ezen.boot_JPA.entity.Board;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface BoardCustomRepository {
    // type, keyword, pageable 값을 주고, Page<Board> list 리턴 받는 메서드 생성

    Page<Board> searchBoard(String type, String keyword, Pageable pageable);

}

 

 BoardCustomRepositoryImpl.java

package com.ezen.boot_JPA.repository;

import com.ezen.boot_JPA.entity.Board;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;

import java.util.List;

import static com.ezen.boot_JPA.entity.QBoard.board;

public class BoardCustomRepositoryImpl implements BoardCustomRepository{

    private final JPAQueryFactory queryFactory;

    public BoardCustomRepositoryImpl(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }

    // 실제 구현
    @Override
    public Page<Board> searchBoard(String type, String keyword, Pageable pageable) {
        // 조건이 많을 경우
        // select * from board where
        // isDel = 'n' and title like '% aaa %';
        // BooleanExpression condition = board.isDel.eq('N')
        // condition = condition.and(board.title.containsIgnoreCase(keyword));
        BooleanExpression condition = null;

        // 동적 검색 조건 추가
        if(type != null && keyword != null){
            // 타입이 여러 개일 경우 추가되는 작업
            String[] typearr = type.split("");
            BooleanExpression dynamicCondition = null;
            for (String t : typearr){
                switch (type) {
                    case "t":
                        condition = (condition == null) ?
                                board.title.containsIgnoreCase(keyword)
                                : condition.or(board.title.containsIgnoreCase(keyword));
                        break;
                    case "w":
                        condition = (condition == null) ?
                                board.writer.containsIgnoreCase(keyword)
                                : condition.or(board.writer.containsIgnoreCase(keyword));
                        break;
                    case "c":
                        condition = (condition == null) ?
                                board.content.containsIgnoreCase(keyword)
                                : condition.or(board.content.containsIgnoreCase(keyword));
                        break;
                }
                if(dynamicCondition != null){
//                  condition = condition.and(dynamicCondition);
                    condition = dynamicCondition;
                }
            }
//            switch (type){
//                case "t" :
//                    condition = board.title.containsIgnoreCase(keyword);
//                    break;
//                case "w" :
//                    condition = board.writer.containsIgnoreCase(keyword);
//                    break;
//                case "c" :
//                    condition = board.content.containsIgnoreCase(keyword);
//                    break;
//                case "tw":
//                    condition = (board.content.containsIgnoreCase(keyword)).or(board.writer.containsIgnoreCase(keyword));
//                    break;
//            }
        }

        // 쿼리 작성 및 페이징 적용
        List<Board> result = queryFactory
                .selectFrom(board)
                .where(condition)
                .orderBy(board.bno.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();
        // 검색된 데이터의 전체 개수 조회
        int total = queryFactory
                .selectFrom(board)
                .where(condition)
                .fetch().size();
        // .fetchCount(); => .fetch().size();
        return new PageImpl<>(result, pageable, total);
    }
}

 

출력