목차 | |
1. | 설정 |
2. | 기초 |
1. 설정
2. 기초
◈ build.gradle (bootJPA)
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'
// https://mvnrepository.com/artifact/nz.net.ultraq.thymeleaf/thymeleaf-layout-dialect
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
implementation 'org.springframework.boot:spring-boot-starter-web'
// https://mvnrepository.com/artifact/org.bgee.log4jdbc-log4j2/log4jdbc-log4j2-jdbc4.1
implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
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'
/*providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'*/
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
◈ application.properties
spring.application.name=boot_JPA
server.port=8089
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:mysql://localhost:3306/bootdb2
spring.datasource.username=springUser
spring.datasource.password=mysql
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.database=mysql
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.thymeleaf.cache=false
◈ log4jdbclog4jdbc.log4j2.properties
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
◈ logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %-5p [%c] %msg%n</pattern>
</encoder>
</appender>
<logger name="com.ezen.boot_JPA" level="INFO" appender-ref="STDOUT"/>
<logger name="jdbc" level="OFF"/>
<logger name="jdbc.connection" level="OFF"/>
<logger name="jdbc.audit" level="OFF"/>
<logger name="jdbc.sqlonly" level="INFO" appender-ref="STDOUT"/>
<logger name="jdbc.resultsettable" level="INFO" appender-ref="STDOUT"/>
<logger name="org.springframework" level="error"/>
<logger name="org.springframework.jdbc" level="error"/>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
◈ BootJpaApplication.java
package com.ezen.boot_JPA;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@SpringBootApplication
public class BootJpaApplication {
public static void main(String[] args) {
SpringApplication.run(BootJpaApplication.class, args);
}
}
◈ layout.html
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="UTF-8">
<title>boot_JPA</title>
<!--/* 부트스트랩 연결 링크 css 연결링크 */-->
<link rel="stylesheet" th:href="@{/dist/css/bootstrap.min.css}">
<script th:src="@{/dist/js/bootstrap.bundle.min.js}"></script>
</head>
<body>
<!--/* th:replace="~{조각 파일의 경로 :: 이름}" */-->
<div th:replace="~{fragments/header :: header}"></div>
<div layout:fragment="content"></div>
<div th:replace="~{fragments/footer :: footer}"></div>
</body>
</html>
◈ header.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!--/* th:fragment="이름" : 레이아웃에서 사용할 조각 */-->
<div th:fragment="header">
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="/">Boot</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/board/register">BoardRegister</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/board/list">BoardList</a>
</li>
</ul>
</div>
</div>
</nav>
</div>
◈ footer.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!--/* th:fragment="이름" : 레이아웃에서 사용할 조각 */-->
<div th:fragment="footer">
<h5>Footer Area</h5>
</div>
◈ index.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>My Spring Boot_JPA Project</h1>
</div>
</div>
◈ Board.java
package com.ezen.boot_JPA.entity;
import jakarta.persistence.*;
import lombok.*;
/* Entity : DB의 테이블 클래스
* DTO : 객체를 생성하는 클래스
* 자주쓰는 코드들 : base class로 별도 관리
* regDate / modDate
*
* id = 기본키
* 기본키 생성 전략 : GeneratedValue
* */
@Entity
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Board extends TimeBase{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increments 생성
private Long bno;
@Column(length = 200, nullable = false)
private String title;
@Column(length = 200, nullable = false)
private String writer;
@Column(length = 2000, nullable = false)
private String content;
// 생성시 초기화 값을 지정 : 객체가 생길 때 객체의 기본값 생성
//@Builder.Default
//private int point = 0;
}
◈ TimeBase.java
package com.ezen.boot_JPA.entity;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@MappedSuperclass
@EntityListeners(value = {AuditingEntityListener.class}) // 반드시 지정
@Getter
public class TimeBase {
/* 등록일, 수정일만 따로 빼서 관리하는 슈퍼 테이블 */
@CreatedDate
@Column(name = "reg_at", updatable = false)
private LocalDateTime regAt;
@LastModifiedDate
@Column(name = "mod_at")
private LocalDateTime modAt;
}
BoardDTO.java
package com.ezen.boot_JPA.dto;
import lombok.*;
import java.time.LocalDateTime;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BoardDTO {
private Long bno;
private String title;
private String writer;
private String content;
private LocalDateTime regAt, modAt;
}
◈ BoardController.java
package com.ezen.boot_JPA.controller;
import com.ezen.boot_JPA.dto.BoardDTO;
import com.ezen.boot_JPA.service.BoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.List;
@Slf4j
@RequestMapping("/board/*")
@RequiredArgsConstructor
@Controller
public class BoardController {
private final BoardService boardService;
@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";
}
@GetMapping("/list")
public void list(Model model){
List<BoardDTO> list = boardService.getList();
model.addAttribute("list", list);
}
@GetMapping("/detail")
public void modify(Model model, @RequestParam("bno") Long bno){
BoardDTO boardDTO = boardService.getDetail(bno);
model.addAttribute("boardDTO", boardDTO);
}
@PostMapping("/modify")
public String modify(BoardDTO boardDTO, RedirectAttributes redirectAttributes){
Long bno = boardService.modify(boardDTO);
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";
}
}
◈ BoardService.java
package com.ezen.boot_JPA.service;
import com.ezen.boot_JPA.dto.BoardDTO;
import com.ezen.boot_JPA.entity.Board;
import java.util.List;
public interface BoardService {
// 추상 메서드만 가능한 인터페이스
// 메서드가 default(접근제한자) 구현 가능
Long insert(BoardDTO boardDTO);
// 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();
}
List<BoardDTO> getList();
BoardDTO getDetail(Long bno);
Long modify(BoardDTO boardDTO);
void delete(Long bno);
}
◈ BoardServiceImpl.java
package com.ezen.boot_JPA.service;
import com.ezen.boot_JPA.dto.BoardDTO;
import com.ezen.boot_JPA.entity.Board;
import com.ezen.boot_JPA.repository.BoardRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Slf4j
@RequiredArgsConstructor
@Service
public class BoardServiceImpl implements BoardService{
private final BoardRepository boardRepository;
@Override
public Long insert(BoardDTO boardDTO) {
// 저장 객체는 Board
// save() : insert 후 저장 객체의 id를 리턴
// save() Entity 객체를 파라미터로 전송
return boardRepository.save(convertDtoToEntity(boardDTO)).getBno();
}
@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 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);
}
}
◈ 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> {
}
◈ BootJpaApplicationTests.java
package com.ezen.boot_JPA;
import com.ezen.boot_JPA.dto.BoardDTO;
import com.ezen.boot_JPA.service.BoardService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = BootJpaApplication.class)
class BootJpaApplicationTests {
@Autowired
private BoardService boardService;
@Test
void contextLoads() {
for(int i=0; i<300; i++){
BoardDTO boardDTO = BoardDTO.builder()
.title("test title " + i)
.writer("tester " + ((int)(Math.random()*50)+1))
.content("Test Content " + i)
.build();
boardService.insert(boardDTO);
}
}
}
◈ 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>
<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>
</div>
</div>
◈ detail.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 Detail Page [[boardDTO.bno]]</h1>
<form action="/board/modify" method="post" id="modForm">
<div class="mb-3">
<label for="r" class="form-label">Created At</label>
<input type="text" class="form-control" name="regDate" id="r" th:value="${boardDTO.regAt}" readonly>
</div><div class="mb-3">
<input type="hidden" class="form-control" name="bno" id="n" th:value="${boardDTO.bno}">
</div>
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" class="form-control" name="title" id="title" th:value="${boardDTO.title}" readonly>
</div>
<div class="mb-3">
<label for="writer" class="form-label">Writer</label>
<input type="text" class="form-control" name="writer" id="writer" th:value="${boardDTO.writer}" readonly>
</div>
<div class="mb-3">
<label for="content" class="form-label">Content</label>
<textarea class="form-control" name="content" id="content" rows="3" readonly>[[${boardDTO.content}]]</textarea>
</div>
<button type="button" id="listBtn" class="btn btn-primary">List</button>
<button type="button" id="modBtn" class="btn btn-warning">Modify</button>
<a th:href="@{/board/delete(bno=${boardDTO.bno})}">
<button type="button" id="delBtn" class="btn btn-danger">Delete</button>
</a>
</form>
</div>
<script th:src="@{/js/boardDetail.js}"></script>
</div>
◈ boardDetail.js
console.log("board detail.js in!!");
document.getElementById('listBtn').addEventListener('click', ()=>{
location.href = "/board/list";
});
document.getElementById('modBtn').addEventListener('click',()=>{
document.getElementById('title').readOnly=false;
document.getElementById('content').readOnly=false;
// 버튼 생성
let modBtn = document.createElement("button");
modBtn.setAttribute("type","submit");
modBtn.classList.add("btn","btn-outline-warning");
modBtn.innerText="Submit";
// 추가
document.getElementById("modForm").appendChild(modBtn);
document.getElementById("modBtn").remove();
document.getElementById("delBtn").remove;
});
▷ 출력
'Java > JPA' 카테고리의 다른 글
JPA 기초(search) - AWS 풀스택 과정 86일차 (0) | 2024.11.27 |
---|---|
JPA 기초(security) - AWS 풀스택 과정 85일차 (0) | 2024.11.26 |
JPA 기초(file) - AWS 풀스택 과정 84일차 (0) | 2024.11.25 |
JPA 기초(comment) - AWS 풀스택 과정 83일차 (0) | 2024.11.22 |