JPA
Mysql 과 JpaData를 활용하여 만든 BoardService
MIN우
2023. 1. 17. 13:29
728x90
1. build gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.7'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
// modelmapper
implementation group: 'org.modelmapper', name: 'modelmapper', version: '2.3.8'
implementation 'mysql:mysql-connector-java'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.projectlombok:lombok:1.18.22'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
2. application.properties
# MySQL ??
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# DB Source URL
spring.datasource.url=jdbc:mysql://localhost:3306/board
# DB username
spring.datasource.username=<id>
# DB password
spring.datasource.password=<pass>
# true ??? JPA ??? ?? ??
spring.jpa.show-sql=true
# DDL(create, alter, drop) ??? DB? ?? ??? ??? ? ??.
spring.jpa.hibernate.ddl-auto=update
# JPA? ???? Hibernate? ????? ??? SQL? ???? ????.
spring.jpa.properties.hibernate.format_sql=true
spring.thymeleaf.cache=false
spring.thymeleaf.check-template-location=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.mvc.hiddenmethod.filter.enabled=true
3. Thymleaf 상세,리스트,수정,쓰기,검색,페이징
detail.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2 th:text="${boardDto.title}"></h2>
<p th:inline="text">작성일 : [[${#temporals.format(boardDto.createdDate, 'yyyy-MM-dd HH:mm')}]]</p>
<p th:text="${boardDto.content}"></p>
<!-- 수정/삭제 -->
<div>
<a th:href="@{'/post/edit/' + ${boardDto.id}}">
<button>수정</button>
</a>
<form id="delete-form" th:action="@{'/post/' + ${boardDto.id}}" method="post">
<input type="hidden" name="_method" value="delete"/>
<button id="delete-btn">삭제</button>
</form>
</div>
<!-- 변수 셋팅 -->
<script th:inline="javascript">
/*<![CDATA[*/
var boardDto = /*[[${boardDto}]]*/ "";
/*]]>*/
</script>
<!-- script -->
<script th:inline="javascript" th:src="@{/js/board.js}"></script>
</body>
</html>
list.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" th:href="@{/css/board.css}">
</head>
<body>
<!-- HEADER -->
<div th:insert="common/header.html" id="header"></div>
<!-- 검색 form -->
<form action="/board/search" method="GET">
<div>
<input name="keyword" type="text" placeholder="검색어를 입력해주세요">
</div>
<button>검색하기</button>
</form>
<a th:href="@{/post}">글쓰기</a>
<table>
<thead>
<tr>
<th class="one wide">번호</th>
<th class="ten wide">글제목</th>
<th class="two wide">작성자</th>
<th class="three wide">작성일</th>
</tr>
</thead>
<tbody>
<!-- CONTENTS !-->
<tr th:each="board : ${boardList}">
<td>
<span th:text="${board.id}"></span>
</td>
<td>
<a th:href="@{'/post/' + ${board.id}}">
<span th:text="${board.title}"></span>
</a>
</td>
<td>
<span th:text="${board.writer}"></span>
</td>
<td>
<span th:text="${#temporals.format(board.createdDate, 'yyyy-MM-dd HH:mm')}"></span>
</td>
</tr>
</tbody>
</table>
<div>
<span th:each="pageNum : ${pageList}" th:inline="text">
<a th:href="@{'/?page=' + ${pageNum}}">[[${pageNum}]]</a>
</span>
</div>
<!-- FOOTER -->
<div th:insert="common/footer.html" id="footer"></div>
</body>
</html>
update.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form th:action="@{'/post/edit/' + ${boardDto.id}}" method="post">
<input type="hidden" name="_method" value="put"/>
<input type="hidden" name="id" th:value="${boardDto.id}"/>
제목 : <input type="text" name="title" th:value="${boardDto.title}"> <br>
작성자 : <input type="text" name="writer" th:value="${boardDto.writer}"> <br>
<textarea name="content" th:text="${boardDto.content}"></textarea><br>
<input type="submit" value="수정">
</form>
</body>
</html>
write.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/post" method="post">
제목 : <input type="text" name="title"> <br>
작성자 : <input type="text" name="writer"> <br>
<textarea name="content"></textarea><br>
<input type="submit" value="등록">
</form>
</body>
</html>
footer.html
<footer>
<h4>Footer 입니다</h4>
</footer>
header.html
<header>
<h1>Header 입니다.</h1>
</header>
board.css
table {
border-collapse: collapse;
}
table, th, td {
border: 1px solid black;
}
header {
background-color: beige;
padding: 3%;
}
footer {
background-color: beige;
padding: 1%;
text-align: center;
}
#wrap {
padding: 15% 0%;
}
#wrap > * {
margin: 3% 0%;
}
.search {
display: inline;
}
a {
text-decoration: none;
}
table {
border-collapse: collapse;
}
table, th, td {
border: 1px solid black;
}
4.ModelMapper 설정
package com.example.boardservice.config;
import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public ModelMapper modelMapper(){
return new ModelMapper();
}
}
5. Controller 설정
package com.example.boardservice.controller;
import com.example.boardservice.Entity.Board;
import com.example.boardservice.dto.BoardDto;
import com.example.boardservice.repository.BoardRepository;
import com.example.boardservice.service.BoardService;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class BoardController {
private final BoardService boardService;
private final BoardRepository boardRepository;
//게시글 목록
@GetMapping("/")
public String list(@RequestParam(value="page", defaultValue = "1") Integer pageNum, Model model){
List<BoardDto> boardList = boardService.getBoardlist(pageNum);
Integer[] pageList = boardService.getPageList(pageNum);
model.addAttribute("boardList", boardList);
model.addAttribute("pageList", pageList);
return "board/list.html";
}
@GetMapping("/post")
public String write(){
return "board/write.html";
}
@PostMapping("/post")
public String write(BoardDto boardDto){
boardService.savePost(boardDto);
return "redirect:/";
}
@GetMapping("/post/{no}")
public String detail(@PathVariable("no") Long no,Model model){
BoardDto boardDto=boardService.getBoardList(no);
model.addAttribute("boardDto",boardDto);
return "board/detail.html";
}
@GetMapping("/post/edit/{no}")
public String edit(@PathVariable("no") Long no, Model model) {
BoardDto boardDTO = boardService.getBoardList(no);
model.addAttribute("boardDto", boardDTO);
return "board/update.html";
}
@PutMapping("/post/edit/{no}")
public String update(BoardDto boardDto){
boardService.savePost(boardDto);
return "redirect:/";
}
@DeleteMapping("/post/{no}")
public String delete(@PathVariable("no") Long no) {
boardService.deletePost(no);
return "redirect:/";
}
@GetMapping("/board/search")
public String search(@RequestParam(value="keyword") String keyword, Model model){
List<BoardDto> boardDtoList = boardService.searchPosts(keyword);
model.addAttribute("boardList",boardDtoList);
return "board/list.html";
}
}
6. dto 설정
package com.example.boardservice.dto;
import com.example.boardservice.Entity.Board;
import lombok.*;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BoardDto {
private Long id;
private String writer;
private String title;
private String content;
private LocalDateTime createdDate;
private LocalDateTime modifiedDate;
public Board toEntity(){
Board build=Board.builder()
.id(id)
.writer(writer)
.title(title)
.content(content)
.build();
return build;
}
}
7. BoardEntity와 TimeEntity 설정 JpaAudting
package com.example.boardservice.Entity;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import javax.persistence.*;
@Entity
@Getter
@NoArgsConstructor(access= AccessLevel.PROTECTED)
@Table(name = "board")
public class Board extends TimeEntity{ //board클래스가 timeentity클래스를 상속받음
@Id @GeneratedValue
private Long id;
@Column(length =10)
private String writer;
@Column(length=100)
private String title;
@Column(columnDefinition = "TEXT")
private String content;
@Builder
public Board(Long id, String writer, String title, String content) {
this.id = id;
this.writer = writer;
this.title = title;
this.content = content;
}
}
package com.example.boardservice.Entity;
import lombok.Getter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass //테이블로 매핑하지않고, 자식Entity에게 매핑정보를 상속하기위한 어노테이션
@EntityListeners(AuditingEntityListener.class) //JPA에게 해당 Entity는 Auditing기능을 사용한다는것을 알리는 어노테이션
public class TimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
8. JpaRepository 설정
package com.example.boardservice.repository;
import com.example.boardservice.Entity.Board;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface BoardRepository extends JpaRepository<Board,Long> {
List<Board> findByTitleContaining(String keyword);
}
9. Service 구현로직
package com.example.boardservice.service;
import com.example.boardservice.Entity.Board;
import com.example.boardservice.dto.BoardDto;
import com.example.boardservice.repository.BoardRepository;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
private final ModelMapper modelMapper;
private static final int BLOCK_PAGE_NUM_COUNT = 5; // 블럭에 존재하는 페이지 번호 수
private static final int PAGE_POST_COUNT = 4; // 한 페이지에 존재하는 게시글 수
//수정 및 글쓰기
@Transactional
public long savePost(BoardDto boardDto) {
return boardRepository.save(boardDto.toEntity()).getId();
}
//리스트 형태로 조회
@Transactional(readOnly = true)
public List<BoardDto> getBoardList(){
return boardRepository.findAll()
.stream()
.map(board->modelMapper.map(board,BoardDto.class))
.collect(Collectors.toList());
}
//단건조회
@Transactional(readOnly = true)
public BoardDto getBoardList(long no){
Optional<Board> BoardList = boardRepository.findById(no);
Board board = BoardList.get();
BoardDto boardDto=BoardDto.builder()
.id(board.getId())
.title(board.getTitle())
.content(board.getContent())
.writer(board.getWriter())
.createdDate(board.getCreatedDate())
.build();
return boardDto;
}
//글 삭제
@Transactional
public void deletePost(Long id) {
boardRepository.deleteById(id);
}
//검색
@Transactional
public List<BoardDto> searchPosts(String keyword){
return boardRepository.findByTitleContaining(keyword)
.stream()
.map(board->modelMapper.map(board, BoardDto.class))
.collect(Collectors.toList());
}
//엔티티로 변환하는작업이 여러개일 경우 함수를 이용해서 처리
private BoardDto convertEntityToDto(Board boardEntity) {
return BoardDto.builder()
.id(boardEntity.getId())
.title(boardEntity.getTitle())
.content(boardEntity.getContent())
.writer(boardEntity.getWriter())
.createdDate(boardEntity.getCreatedDate())
.build();
}
@Transactional
public List<BoardDto> getBoardlist(Integer pageNum) {
Page<Board> page = boardRepository.findAll(PageRequest.of(pageNum - 1, PAGE_POST_COUNT, Sort.by(Sort.Direction.ASC, "createdDate")));
List<Board> boardEntities = page.getContent();
List<BoardDto> boardDtoList = new ArrayList<>();
for (Board boardEntity : boardEntities) {
boardDtoList.add(this.convertEntityToDto(boardEntity));
}
return boardDtoList;
}
@Transactional
public Long getBoardCount() {
return boardRepository.count();
}
public Integer[] getPageList(Integer curPageNum) {
Integer[] pageList = new Integer[BLOCK_PAGE_NUM_COUNT];
// 총 게시글 갯수
Double postsTotalCount = Double.valueOf(this.getBoardCount());
// 총 게시글 기준으로 계산한 마지막 페이지 번호 계산 (올림으로 계산)
Integer totalLastPageNum = (int)(Math.ceil((postsTotalCount/PAGE_POST_COUNT)));
// 현재 페이지를 기준으로 블럭의 마지막 페이지 번호 계산
Integer blockLastPageNum = (totalLastPageNum > curPageNum + BLOCK_PAGE_NUM_COUNT)
? curPageNum + BLOCK_PAGE_NUM_COUNT
: totalLastPageNum;
// 페이지 시작 번호 조정
curPageNum = (curPageNum <= 3) ? 1 : curPageNum - 2;
// 페이지 번호 할당
for (int val = curPageNum, idx = 0; val <= blockLastPageNum; val++, idx++) {
pageList[idx] = val;
}
return pageList;
}
}
10. EnableJpaAuditing 설정
package com.example.boardservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@SpringBootApplication
public class BoardServiceApplication {
public static void main(String[] args) {
SpringApplication.run(BoardServiceApplication.class, args);
}
}
김영한 개발팀장님의 강의를 듣고 , Entity는 절대 노출시켜서는 안된다.
Getter는 사용해도 되지만, Setter는 가급적으로 사용하지말고 간단하게 구현을 해보았습니다.
728x90