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