본문 바로가기

SpringBoot

SpringBoot(27) - 복습 예제(게시판 글 목록 페이징 / 검색 선택 / 글 읽기)

728x90
반응형

1) SpringBoot

   1-1) 복습 예제

      1-1-1) 게시판 글 목록 페이징 / 검색 선택 / 글 읽기 기능 구현 

 

 

 

 

 

1) SpringBoot

1-1) 복습 예제

[src/main/resources] 안의 application.properties 파일에 아래와 같이 내용 추가

spring.application.name=web5

#접속 포트번호
server.port=8888
#Context Path
server.servlet.context-path=/

#Logback 사용. 전체를 대상으로 로깅 레벨 지정
#error>warn>info>debug>trace
logging.level.root=info
#특정 패키지를 대상으로 로깅 레벨 지정
logging.level.net.datasa.web5=debug

# MySQL 데이터베이스 설정
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Seoul&useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root

# JPA 설정
# DB 구조와 Entity가 안 맞을 때 에러를 내주는 역할을 하는 것이 바로 및의 "~ddl-auto=validate" 설정이다.
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.type.descriptor.sql=trace

spring.jackson.time-zone=Asia/Seoul

# 게시판 관련 설정
board.pageSize=10
board.linkSize=2
board.uploadPath=c:/upload

 

 

 

1-1-1) 게시판 글 목록 페이징 / 검색 선택 / 글 읽기 기능 구현

(1) [src/main/resources] - [templates] 안에 home.html 파일 생성 후 아래와 같이 작성

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org"
		xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
	<meta charset="UTF-8">
	<title>web5</title>
</head>
<body>
	<h1>[ web5 ]</h1>
	<p sec:authorize="isAuthenticated()">
		<span th:text="${#authentication.name}"></span>
		님 환영합니다~~!
	</p>
	
	<div sec:authorize="not isAuthenticated()">
		<p>
			<a th:href="@{/member/joinForm}">회원가입</a>
		</p>
		<p>
			<a th:href="@{/member/loginForm}">로그인</a>
		</p>
	</div>
	
	<div sec:authorize="isAuthenticated()">
		<p>
			<a th:href="@{/member/logout}">로그아웃</a>
		</p>
		<p>
			<a th:href="@{/member/info}">개인정보 수정</a>
		</p>
	</div>
	
	<p>
		<a th:href="@{/board/list}">게시판</a>
	</p>
	
</body>
</html>

 

 

(2) [src/main/java] - [net.datasa.web5.repository] 안에 BoardRepository.java 파일 생성(반드시 Interface로 생성할 것!!) 후 아래와 같이 작성

package net.datasa.web5.repository;

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import net.datasa.web5.domain.entity.BoardEntity;

@Repository
public interface BoardRepository extends JpaRepository<BoardEntity, Integer> {
	
	List<BoardEntity> findByTitleContaining(String s, Sort sort);
	List<BoardEntity> findTop3OByTitleContainingOrderByBoardNum(String s);
	
	// 전달된 문자열을 제목에서 검색한 후 지정한 한 페이지 분량 리턴
	Page<BoardEntity> findByTitleContaining(String s, Pageable p);
	
	// 전달된 문자열을 본문에서 검색한 후 지정한 한 페이지 분량 리턴
	Page<BoardEntity> findByContentsContaining(String s, Pageable p);
	
	// 전달된 문자열을 작성자 ID에서 검색한 후 지정한 한 페이지 분량 리턴
	Page<BoardEntity> findByMember_MemberId(String s, Pageable p);
}

 

 

(3) [src/main/java] - [net.datasa.web5.controller] 안에 BoardController.java 파일 생성 후 아래와 같이 작성

package net.datasa.web5.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.datasa.web5.domain.dto.BoardDTO;
import net.datasa.web5.security.AuthenticatedUser;
import net.datasa.web5.service.BoardService;

@Slf4j
@RequestMapping("board")
@RequiredArgsConstructor
@Controller
public class BoardController {
	
	private final BoardService boardService;
	
	@Value("${board.pageSize}")
	int pageSize;
	
	@Value("${board.linkSize}")
	int linkSize;
	
	@Value("${board.uploadPath}")
	String uploadPath;
	
	// 메인화면에서 "board/list" 경로를 클릭했을 때 처리하는 메소드
	// templates/boardView/list.html 파일로 포워딩
	// 로그인 안 한 상태에서 해당 페이지의 "게시판"이라는 제목이 보여야 함
	/**
	 * 글 목록 보기
	 * @return 글 목록 출력 HTML 파일
	 * */
	@GetMapping("list")
	public String list(Model model,
			@RequestParam(name="page", defaultValue = "1") int page,
			@RequestParam(name="searchType", defaultValue = "") String searchType,
			@RequestParam(name="searchWord", defaultValue = "") String searchWord) {
		
		log.debug("properties의 값 : pageSize={}, linkSize={}, uploadPath={}",
				pageSize, linkSize, uploadPath);
		log.debug("요청 파라미터 : page={}, searchType={}, searchWord={}",
				page, searchType, searchWord);
		
		// 서비스로 (페이지, 페이지당 글 수, 검색조건, 검색어) 전달하여 결과를 Page 객체로 받음
		Page<BoardDTO> boardPage = boardService.getList(page, pageSize, searchType, searchWord);
		
		// Page 객체 내의 정보들
		log.debug("목록정보 getContent() : {}", boardPage.getContent());
		log.debug("현재페이지 getNumber() : {}", boardPage.getNumber());
		log.debug("전체 개수 getTotalElements() : {}", boardPage.getTotalElements());
		log.debug("전체 페이지수 getTotalPages() : {}", boardPage.getTotalPages());
		log.debug("한페이지당 글 수 getSize() : {}", boardPage.getSize());
		log.debug("이전페이지 존재 여부 hasPrevious() : {}", boardPage.hasPrevious());
		log.debug("다음페이지 존재 여부 hasNext() : {}", boardPage.hasNext());
		
		// HTML로 포워딩하기 전에 모델에 필요 정보들을 저장
		model.addAttribute("boardPage", boardPage);
		model.addAttribute("page", page);
		model.addAttribute("linkSize", linkSize);
		model.addAttribute("searchType", searchType);
		model.addAttribute("searchWord", searchWord);
		
		// HTML로 포워딩
		return "boardView/list";
		
		/*
		// 서비스에서 전체 글 목록 전달받음
		List<BoardDTO> boardList = boardService.getList(searchWord);
		
		log.debug("전달된 글 목록 : {}", boardList);
		
		// 글 목록을 모델에 저장하고 HTML로 포워딩하여 출력
		model.addAttribute("boardList", boardList);
		
		return "boardView/list";
		*/

	}
	
	/**
	 * 글쓰기 폼으로 이동
	 * @return 글쓰기 폼을 출력하는 HTML 파일
	 * */
	@GetMapping("write")
	public String write() {
		return "boardView/writeForm";
	}
	
	@PostMapping("write")
	public String join(@AuthenticationPrincipal AuthenticatedUser user,
			@ModelAttribute BoardDTO boardDTO) {
		log.debug("전달된 글정보 : {}", boardDTO);
		
		boardDTO.setMemberId(user.getUsername());
		
		log.debug("로그인한 아이디 추가한 글정보 : {}", boardDTO);
		
		// 서비스로 전달하여 저장
		boardService.saveWrite(boardDTO);
		
		return "redirect:list";
	}
	
	@GetMapping("read" + "{boardNum}")
	public String read(@ModelAttribute BoardDTO boardDTO,
			Model model,
			@PathVariable("boardNum") String boardNum) {
		
		Integer boardNumber = boardDTO.getBoardNum();
		
		BoardDTO dto = boardService.readBoard(boardNumber);
		
		log.debug("선택한 글 정보 : {}", dto);
		
		// 해당 게시글을 모델에 저장하고 HTML로 포워딩하여 출력
		model.addAttribute("boardDTO", dto);
		
		return "boardView/readForm";
	}
	
}

 

 

(4) [src/main/java] - [net.datasa.web5.service] 안에 BoardService.java 파일 생성 후 아래와 같이 작성

package net.datasa.web5.service;

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 jakarta.persistence.EntityNotFoundException;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.datasa.web5.domain.dto.BoardDTO;
import net.datasa.web5.domain.entity.BoardEntity;
import net.datasa.web5.domain.entity.MemberEntity;
import net.datasa.web5.repository.BoardRepository;
import net.datasa.web5.repository.MemberRepository;

/**
 * 게시판 관련 서비스
 * */
@Slf4j
@RequiredArgsConstructor
@Service
// @Transactional : Commit, Rollback을 감지하는 역할을 하는 Annotation
@Transactional
public class BoardService {
	
	private final BoardRepository boardRepository;
	private final MemberRepository memberRepository;

	/**
	 * 게시판 글 저장
	 * @param boardDTO 게시판 글 정보
	 * */
	public void saveWrite(BoardDTO boardDTO) {
		MemberEntity memberEntity = memberRepository.findById(boardDTO.getMemberId())
				.orElseThrow(() -> new EntityNotFoundException("아이디가 없습니다."));
		
		BoardEntity entity = BoardEntity.builder()
				// DTO로 전달받은 값들을 Entity에 세팅
				// .memberId(boardDTO.getMemberId())
				.member(memberEntity)
				.title(boardDTO.getTitle())
				.contents(boardDTO.getContents())
				// 기타 추가 데이터를 Entity에 세팅
				.viewCount(0)
				.likeCount(0)
				.build();
		
		log.debug("저장되는 entity : {}", entity);
		
		// DB에 저장
		boardRepository.save(entity);
		
	}

	/**
	 * 검색 결과 글 목록을 지정한 한 페이지 분량의 Page 객체로 리턴
	 * @param page         현재 페이지
	 * @param pageSize     페이지당 글 수
	 * @param searchType   검색조건(제목 검색: title, 본문 검색: contents, 작성자 검색: id)
	 * @param searchWord   검색어
	 * @return             게시글 목록 정보
	 * */
	public Page<BoardDTO> getList(int page, int pageSize, String searchType, String searchWord) {
		// 조회 조건을 담은 Pageable 객체 생성
		// page의 경우, 배열의 인덱스처럼 0부터 시작하기에 1페이지(인덱스로 0페이지에 해당)를 가져오기 위해 "page - 1"로 작성함
		Pageable p = PageRequest.of(page - 1, pageSize, Sort.Direction.DESC, "boardNum");
		
		Page<BoardEntity> entityPage;
		
		// repository의 메소드로 Pageable 전달하여 조회. Page 리턴받음.		
		if (searchType.equals("title")) {
			entityPage = boardRepository.findByTitleContaining(searchWord, p);
		} else if (searchType.equals("contents")) {
			entityPage = boardRepository.findByContentsContaining(searchWord, p);
		} else if (searchType.equals("id")) {
			entityPage = boardRepository.findByMember_MemberId(searchWord, p);
		} else {
			entityPage = boardRepository.findAll(p);
		}
		
		log.debug("조회된 결과 Entity 페이지 : {}", entityPage.getContent());
		
		// entityPage 객체 내의 Entity들을 DTO 객체로 변환하여 새로운 Page 객체 생성
		Page<BoardDTO> dtoPage = entityPage.map(this::convertToDTO);
		
		return dtoPage;
	}
	
	private BoardDTO convertToDTO(BoardEntity entity) {
		return BoardDTO.builder()
		.boardNum(entity.getBoardNum())
		.memberId(entity.getMember().getMemberId())
		.memberName(entity.getMember().getMemberName())
		.title(entity.getTitle())
		.contents(entity.getContents())
		.viewCount(entity.getViewCount())
		.likeCount(entity.getLikeCount())
		.originalName(entity.getOriginalName())
		.fileName(entity.getFileName())
		.createDate(entity.getCreateDate())
		.updateDate(entity.getUpdateDate())
		.build();
	}

	public BoardDTO readBoard(Integer boardNum) {
		BoardEntity entity = boardRepository.findById(boardNum)
				.orElseThrow(() -> new EntityNotFoundException("글이 없습니다."));
		
		return convertToDTO(entity);
	}

	
	/**
	 * 게시판의 글 목록 조회
	 * @return 글 목록 정보
	 * */
	/*
	public List<BoardDTO> getList(String searchWord) {
		// BoardRepository의 메소드를 호출하여 게시판의 모든 글 정보를 조회
		// Entity의 개수만큼 반복하면서 Entity의 값을 BoardDTO 객체를 생성하여 저장
		// 생성된 BoardDTO 객체를 ArrayList에 저장
		// 최종 완성된 ArrayList 객체를 리턴함
		
		// Sort : SQL의 order by 구문을 만들어주는 역할
		Sort sort = Sort.by(Sort.Direction.DESC, "boardNum");
		
		List<BoardEntity> entityList = boardRepository.findByTitleContaining(searchWord, sort);

		// List<BoardEntity> entityList = boardRepository.findTop3ByTitleContainingOrderByBoardNum(searchWord);
		
		// List<BoardEntity> entityList = boardRepository.findAll(sort);
		
		List<BoardDTO> boardDTOList = new ArrayList<>();
				
		// 반복문으로 Entity 객체를 DTO로 변환해서 ArrayList에 저장
		for (BoardEntity entity : entityList) {
			BoardDTO dto = BoardDTO.builder()
					.boardNum(entity.getBoardNum())
					.memberId(entity.getMember().getMemberId())
					.memberName(entity.getMember().getMemberName())
					.title(entity.getTitle())
					.viewCount(entity.getViewCount())
					.createDate(entity.getCreateDate())
					.updateDate(entity.getUpdateDate())
					.build();
			boardDTOList.add(dto);
		}
		
		return boardDTOList;
	}
	*/

}

 

 

(5) [src/main/resources] - [templates.boardView] 안에 list.html 파일 생성 후 아래와 같이 작성

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org"
	xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
	<meta charset="UTF-8">
	<title>게시판</title>
	<style>
		#listArea {
			width: 1000px;
			margin: 0 auto;
			text-align: center;
		}
		
		a {
			text-decoration-line: none;
			color: black;
		}
	
		h1 {
			text-align: center;
		}
	
		table, th, tr, td {
			border: 2px solid black;
        	border-collapse: collapse;
        	padding: 10px;
        	text-align: center;
		}
		
		.white {
			border: none;
		}
		
		.head {
			border: none;
		}
		
		th {
			background-color: grey;
			color: white;
		}
		
		td {
			width: 100px;
		}
		
		.title {
			width: 300px;
		}
		
		.date {
			width: 130px;
		}
		
		#pageArea {
			margin: 30px 0;
		}

	</style>
	
	<script th:src="@{/js/jquery-3.7.1.min.js}"></script>
	
	<!-- 페이지 이동 스크립트  -->
	<script>
		function pagingFormSubmit(currentPage) {
			$('#page').val(currentPage);
			$('#pagingForm').submit();
		}
	</script>
</head>
<body>
	<div id="listArea">
		<h1>
			<a th:href="@{/board/list}">[ 게시판 ]</a>
		</h1>
		
		<!--/*
			<div>[[${boardPage.getContent()}]]</div>
			<div>[[${boardPage.getTotalElements()}]]</div>
			<div>[[${boardPage.getTotalPages()}]]</div>
			<div>[[${boardPage.getNumber()}]]</div>
			<div>[[${boardPage.getSize()}]]</div>
			<div>[[${boardPage.hasPrevious()}]]</div>
			<div>[[${boardPage.hasNext()}]]</div>
		*/-->
		
		<!-- 글목록 출력 영역 -->
		<div id="list">
			<table style="border: none;">
				<tr class="white">
					<td class="white">
						전체 <span th:text="${boardPage.totalElements}"></span>
					</td>
					<td class="white">페이지 <span th:text="${page}"></span> of <span th:text="${boardPage.getTotalPages()}"></span></td>
					<td class="white" colspan="4"></td>
					<td class="head">
						<a sec:authorize="isAuthenticated()" th:href="@{/board/write}">글쓰기</a>
						<a th:href="@{/}">HOME</a>
					</td>
				</tr>
				<tr>
					<th>번호</th>
					<th class="title">제목</th>
					<th>작성자 ID</th>
					<th>작성자명</th>
					<th>조회수</th>
					<th class="date">작성일</th>
					<th class="date">수정일</th>
				</tr>
				<tr th:each="board, n : ${boardPage}">
					<td th:text="${board.boardNum}"></td>
					<td>
						<a th:text="${board.title}" th:href="@{/board/read(boardNum=${board.boardNum})}" class="title"></a>
					</td>
					<td th:text="${board.memberId}"></td>
					<td th:text="${board.memberName}"></td>
					<td th:text="${board.viewCount}"></td>
					<!-- LocalDateTime 타입으로 가지고 온 값은 아래와 같이 "temporals.format()"을 써서 형식을 지정해야 함 -->
					<td th:text="${#temporals.format(board.createDate, 'yy.MM.dd HH:mm')}" class="date"></td>
					<td th:text="${#temporals.format(board.updateDate, 'yy.MM.dd HH:mm')}" class="date"></td>
				</tr>
			</table>
		</div>
		
		<div id="pageArea">
			<a
				th:if="${boardPage.hasPrevious() and boardPage.getNumber() > 4}"
				th:href="|javascript:pagingFormSubmit(${page - 5})|">◁◁</a>
			<a
				th:if="${boardPage.hasPrevious()}"
				th:href="|javascript:pagingFormSubmit(${page - 1})|">◀</a>
			
			<!-- 페이지 이동 링크 -->
			<span
				th:if="${boardPage.getTotalPages() > 0}"
				th:each="counter : ${#numbers.sequence((page - linkSize < 1 ? 1 : page - linkSize), (page + linkSize > boardPage.getTotalPages() ? boardPage.getTotalPages() : page + linkSize))}">
				<th:block th:if="${counter == page}"><b></th:block>
					<a th:text="${counter}" th:href="|javascript:pagingFormSubmit(${counter})|"></a>&nbsp;
				<th:block th:if="${counter == page}"></b></th:block>
			</span>
			
			<a
				th:if="${boardPage.hasNext()}"
				th:href="|javascript:pagingFormSubmit(${page + 1})|">▶</a>
			<a
				th:if="${boardPage.hasNext() and boardPage.getNumber() < boardPage.getTotalPages() - 5}"
				th:href="|javascript:pagingFormSubmit(${page + 5})|">▷▷</a>
		</div>
		
		<!-- 검색폼 -->
		<div id="searchArea">
			<form id="pagingForm" method="get" th:action="@{/board/list}">
				<input type="hidden" name="page" id="page" />
				<select id="type" name="searchType">
					<option value="title" th:selected="${searchType == 'title'}">제목</option>
					<option value="contents" th:selected="${searchType == 'contents'}">본문</option>
					<option value="id" th:selected="${searchType == 'id'}">작성자ID</option>
				</select>
				<input type="text"  name="searchWord" th:value="${searchWord}">
				<input type="button" onclick="pagingFormSubmit(1)" value="검색">
			</form>
		</div>
		
	</div>
	
</body>
</html>

 

 

(6) [src/main/resources] - [templates.boardView] 안에 readForm.html 파일 생성 후 아래와 같이 작성

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org"
	xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
	<meta charset="UTF-8">
	<title>게시글 읽기</title>
	<style>
		#readArea {
			width: 900px;
			margin: 0 auto;
			text-align: center;
		}
		
		#readForm {
			display: flex;
			flex-direction: column;
			justify-content: center;
		}
			
		#formArea {
			display: flex;
			justify-content: center;
		}
		
		table, tr, td {
			border: 2px solid black;
        	border-collapse: collapse;
        	padding: 10px;
        	margin: 20px 0 15px 0;
		}
		
		th {
			width: 100px;
			height: 40px;
			background-color: grey;
			color: white;
		}
		
		td {
			width: 300px;
			height: 40px;
			text-align: left;
		}
		
	</style>
	<script th:src="@{/js/jquery-3.7.1.min.js}"></script>
	<script></script>
</head>
<body>
	<div id="readArea">
		<h1>[ 게시글 읽기 ]</h1>
		
		<div id="readForm">
			<div id="formArea">
				<table>
					<tr>
						<th>작성자</th>
						<td th:text="${boardDTO.memberId}"></td>
					</tr>
					<tr>
						<th>작성일</th>
						<td th:text="${#temporals.format(boardDTO.createDate, 'yyyy.MM.dd HH:mm:ss')}"></td>
					</tr>
					<tr>
						<th>조회수</th>
						<td th:text="${boardDTO.viewCount}"></td>
					</tr>
					<tr>
						<th>제목</th>
						<td th:text="${boardDTO.title}"></td>
					</tr>
					<tr>
						<th>내용</th>
						<td th:text="${boardDTO.contents}"></td>
					</tr>
					<tr>
						<th>파일첨부</th>
						<td th:text="${boardDTO.fileName}"></td>
					</tr>
				</table>
			</div>
			
			
	        <div id="btnArea">
	        	<a href="#" id="updateBtn">수정</a>
	        	<a href="#" id="deleteBtn">삭제</a>
	        </div>

		</div>
	</div>
</body>
</html>

 

 

(7) 결과 화면

첫 접속 화면 : 메인 페이지, 게시판 글 목록 페이지는 로그인 안해도 볼 수 있음

 

 

 

게시판 글 목록 화면

- 페이징 기능 : 이전/다음 페이지 모두 이동 가능, 이동 시 페이지 단위(한번에 1페이지씩 / 5페이지씩)별로 이동 가능

 

 

- 검색 선택 기능 : 원하는 검색조건(제목 / 본문 / 작성자ID)을 선택하여 글 목록 조회 가능

 

 

게시판 글 읽기 화면 : 글 목록에서 각 게시글의 제목란을 클릭하여 해당 글 정보 조회 가능