본문 바로가기

SpringBoot

SpringBoot(26) - 복습 예제(게시판 글 목록 보기)

728x90
반응형

1) SpringBoot

   1-1) 복습 예제

      1-1-1) 게시판 글 목록 보기 기능 구현 

 

 

 

 

 

1) SpringBoot

1-1) 복습 예제

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.domain.entity] 안에 BoardEntity.java 파일 생성 후 아래와 같이 작성

package net.datasa.web5.domain.entity;

import java.time.LocalDateTime;

import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name="web5_board")
@EntityListeners(AuditingEntityListener.class)
public class BoardEntity {
	
	/*
	board_num         int            auto_increment primary key,    		-- 게시글 일련번호  
	member_id         varchar(30),       						            -- 작성자 ID(외래키 : web5_member 테이블 참조)      
	title       	  varchar(1000)  not null,       						-- 글제목     
	contents          text           not null,                   			-- 글내용  
	view_count        int            default 0,                   			-- 조회수
	like_count        int            default 0,                  			-- 추천수
	original_name     varchar(300),                                         -- 첨부파일 원래 이름
	file_name         varchar(100),                                         -- 첨부파일 저장된 이름
	create_date       timestamp      default current_timestamp,             -- 작성 시간
	update_date       timestamp      default current_timestamp
										on update current_timestamp,        -- 수정 시간
	constraint foreign key (member_id)
    			references web5_member(member_id) on delete set null
	*/
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name="board_num")
	private Integer boardNum;
	
	// 작성자 아이디
	// @Column(name="member_id", length = 30)
	// private String memberId;
	
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name="member_id", referencedColumnName = "member_id")
	MemberEntity member;
	
	// 글 제목
	@Column(name="title", nullable = false, length = 1000)
	private String title;
	
	// 글 내용
	@Column(name="contents", nullable = false, columnDefinition = "text")
	private String contents;
	
	// 조회수
	@Column(name="view_count", columnDefinition = "int default 0")
	private Integer viewCount;
	
	// 추천수
	@Column(name="like_count", columnDefinition = "int default 0")
	private Integer likeCount;
	
	// 첨부파일 원래 이름
	@Column(name="original_name", length = 300)
	private String originalName;
	
	// 첨부파일 저장된 이름
	@Column(name="file_name", length = 100)
	private String fileName;
	
	// 작성 시간
	@CreatedDate
	@Column(name="create_date", columnDefinition = "timestamp default current_timestamp")
	private LocalDateTime createDate;
	
	// 수정 시간
	@LastModifiedDate
	@Column(name="update_date", columnDefinition = "timestamp default current_timestamp on update current_timestamp")
	private LocalDateTime updateDate;
}

 

 

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

package net.datasa.web5.domain.dto;

import java.time.LocalDateTime;

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

/**
 * 게시글 정보 DTO
 * */
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BoardDTO {
	// Wrapper Class(Integer 등) : 기본 자료형을 포장해서 객체로 만들어 줌
	private Integer boardNum;            // 게시글 일련번호
	private String memberId;             // 작성자 아이디(외래키)
	private String memberName;           // 작성자 이름(외래키를 통해 가지고 온 내용 중 이름)
	private String title;                // 글 제목
	private String contents;             // 글 내용
	private Integer viewCount;           // 조회수
	private Integer likeCount;           // 추천수
	private String originalName;         // 첨부파일 원래 이름
	private String fileName;             // 첨부파일 저장된 이름
	private LocalDateTime createDate ;   // 작성 시간
	private LocalDateTime updateDate;    // 수정 시간
}

 

 

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

package net.datasa.web5.repository;

import java.util.List;

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

 

 

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

package net.datasa.web5.controller;

import java.util.List;

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.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;
	
	// 메인화면에서 "board/list" 경로를 클릭했을 때 처리하는 메소드
	// templates/boardView/list.html 파일로 포워딩
	// 로그인 안 한 상태에서 해당 페이지의 "게시판"이라는 제목이 보여야 함
	/**
	 * 글 목록 보기
	 * @return 글 목록 출력 HTML 파일
	 * */
	@GetMapping("list")
	public String list(Model model,
			@RequestParam(name="searchWord", defaultValue = "") String searchWord) {
		// 서비스에서 전체 글 목록 전달받음
		List<BoardDTO> boardList = boardService.getList(searchWord);
		
		log.debug("전달된 글 목록 : {}", boardList);
		
		// 글 목록을 모델에 저장하고 HTML로 포워딩하여 출력
		model.addAttribute("boardList", boardList);
		
		return "boardView/list";
	}
	
	@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";
	}
	
}

 

 

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

package net.datasa.web5.service;

import java.util.ArrayList;
import java.util.List;

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

	/**
	 * 게시판의 글 목록 조회
	 * @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;
	}

}

 

 

(7) [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;
		}
	
		h1 {
			text-align: center;
		}
		
		#menu {
			display: flex;
			justify-content: space-between;
			font-weight: bold;
		}
		
		#menu > #link {
			display: inline-block;
			margin-left: 830px;
		}
		
		#menu > #link > a {
			text-decoration-line: none;
			color: blue;
		}
		
		#menu > #link > #home {
			display: inline-block;
			margin-left: 20px;
		}
	
		table, th, tr, td {
			border: 2px solid black;
        	border-collapse: collapse;
        	padding: 10px;
        	margin: 20px 0 15px 0;
        	text-align: center;
		}
		
		th {
			background-color: grey;
			color: white;
		}
		
		td {
			width: 100px;
		}
		
		.title {
			width: 300px;
		}
		
		.date {
			width: 130px;
		}

	</style>
	<script th:src="@{/js/jquery-3.7.1.min.js}"></script>
	<script></script>
</head>
<body>
	<div id="listArea">
		<h1>[ 게시판 ]</h1>
		
		<div id="menu">
			<span th:each="board, num : ${boardList}" id="boardTotalNum">
				<th:block th:if="${num.last}">
					<span>전체 </span>
					<span th:text="${num.size}"></span>
				</th:block>
			</span>
			<span id="link">
				<a th:href="@{/board/write}" id="write">글쓰기</a>
				<a th:href="@{/}" id="home">Home</a>
			</span>
		</div>
		
		<div id="list">
			<table>
				<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 : ${boardList}">
					<td th:text="${board.boardNum}"></td>
					<td th:text="${board.title}" class="title"></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, 'yyyy-MM-dd')}" class="date"></td>
					<td th:text="${#temporals.format(board.updateDate, 'yyyy-MM-dd')}" class="date"></td>
				</tr>
			</table>
		</div>
		
		<div id="searchArea">
			<form action="list" method="get" >
				<input type="text" name="searchWord">
				<input type="submit" value="검색">
			</form>
		</div>
		
	</div>
	
</body>
</html>

 

 

(8) 결과 화면

첫 접속 화면 : 메인 페이지, 게시판 글 목록 페이지는 로그인 안해도 볼 수 있음(단, 글쓰기를 하려면 로그인해야 함)

 

 

게시판 글 목록 화면

- 글 검색 기능 : 검색한 키워드를 포함하고 있는 제목을 가진 글만 목록 화면에 출력해줌

(SQL의 LIKE 연산자 기능과 동일함)

 

 

※ 참고(Repository에 DB에 대한 SQL Query문 역할을 수행할 메소드 정의 시 규칙 확인하는 루트)

spring.io(https://spring.io/)

>>  [ Projects ] - [ Spring Data ]

>>  [ Spring Data JPA ] - [ LEARN ] - [ Reference Doc. ]

>>  [ JPA ] - [ JPA Query Methods ]

 

<!-- LocalDateTime 타입으로 가지고 온 값은 아래와 같이 "temporals.format()"을 써서 형식을 지정해야 함 -->
<td th:text="${#temporals.format(board.createDate, 'yyyy-MM-dd')}" class="date"></td>
<td th:text="${#temporals.format(board.updateDate, 'yyyy-MM-dd')}" class="date"></td>