자바 애플리케이션에서 관계형 데이터베이스와 객체 간의 매핑을 제공하는 표준 API
JPA는 자바 객체를 데이터베이스 테이블에 매핑하고, 자바 객체와 데이터베이스 간의 데이터 전송을 관리
JPA를 사용하면 데이터베이스 작업을 쉽게 처리할 수 있으며, SQL 코드를 줄이고 객체 지향적인 방식으로 데이터를 다룰 수 있음
- MSA 환경에서 서비스 별로 디비 분리 -> 디비 연관관계 단순화
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
데이터베이스 테이블에 매핑되는 자바 클래스
@Entity
애노테이션으로 정의
BoardEntity
package board.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "t_jpa_board")
@Data
@DynamicUpdate // 변경된 필드만 업데이트
public class BoardEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO) // 자동 증가. AUTO or IDENTITY
private int boardIdx;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String contents;
@Column(nullable = false)
private int hitCnt;
@CreationTimestamp
@Column(nullable = false, updatable = false)
private LocalDateTime createdDt;
@Column(nullable = false, updatable = false)
private String createdId;
@UpdateTimestamp
private LocalDateTime updatorDt;
private String updatorId;
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) // (1 : n) / EAGER : 조인. (함께 조회) / Lazy는 지연 로딩.
@JoinColumn(name = "board_idx")
private List<BoardFileEntity> fileInfoList;
}
BoardFileEntity
package board.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.DynamicUpdate;
@Entity
@Data
@Table(name = "t_jpa_file")
@DynamicUpdate
public class BoardFileEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int idx;
// private int boardIdx;
@Column(nullable = false)
private String originalFileName;
@Column(nullable = false)
private String storedFilePath;
@Column(nullable = false)
private String fileSize;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_idx") // 외래키
private BoardEntity board;
}
package board.repository;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import board.entity.BoardEntity;
import board.entity.BoardFileEntity;
@Repository
public interface JpaBoardRepository extends CrudRepository<BoardEntity, Integer> {
// 쿼리 메서드
List<BoardEntity> findAllByOrderByBoardIdxDesc();
// @Query 어노테이션으로 실행할 쿼리를 직접 정의
@Query("SELECT file FROM BoardFileEntity file WHERE file.board.boardIdx = :boardIdx AND file.idx = :idx")
BoardFileEntity findBoardFile(@Param("boardIdx") int boardIdx, @Param("idx") int idx);
}
Dto 대신 Entity 사용
JpaBoardService
package board.service;
import java.util.List;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import board.entity.BoardEntity;
import board.entity.BoardFileEntity;
public interface JpaBoardService {
List<BoardEntity> selectBoardList();
BoardEntity selectBoardDetail(int boardIdx);
void insertBoard(BoardEntity boardEntity, MultipartHttpServletRequest request) throws Exception;
void updateBoard(BoardEntity boardEntity);
// void saveBoard(BoardEntity boardEntity, MultipartHttpServletRequest request) throws Exception;
void deleteBoard(int boardIdx);
BoardFileEntity selectBoardFileInfo(int idx, int boardIdx);
}
JpaBoardServiceImpl
package board.service;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import board.common.FileUtils;
import board.entity.BoardEntity;
import board.entity.BoardFileEntity;
import board.repository.JpaBoardRepository;
@Service
public class JpaBoardServiceImpl implements JpaBoardService {
@Autowired
private JpaBoardRepository jpaBoardRepository;
@Autowired
private FileUtils fileUtils;
@Override
public List<BoardEntity> selectBoardList() {
return jpaBoardRepository.findAllByOrderByBoardIdxDesc();
}
@Override
public BoardEntity selectBoardDetail(int boardIdx) {
Optional<BoardEntity> optional = jpaBoardRepository.findById(boardIdx);
if (optional.isPresent()) {
BoardEntity board = optional.get();
board.setHitCnt(board.getHitCnt() + 1);
jpaBoardRepository.save(board);
return board;
} else {
throw new RuntimeException();
}
}
/*
@Override
public void saveBoard(BoardEntity boardEntity, MultipartHttpServletRequest request) throws Exception {
boardEntity.setCreatedId("admin");
BoardEntity existingBoard = jpaBoardRepository.findById(boardEntity.getBoardIdx()).orElse(null);
if (existingBoard != null) {
// boardEntity.setFileInfoList(existingBoard.getFileInfoList());
} else {
List<BoardFileEntity> list = fileUtils.parseFileInfo2(boardEntity.getBoardIdx(), request);
if (!CollectionUtils.isEmpty(list)) {
boardEntity.setFileInfoList(list);
}
}
jpaBoardRepository.save(boardEntity);
}
*/
@Override
public void insertBoard(BoardEntity boardEntity, MultipartHttpServletRequest request) throws Exception {
boardEntity.setCreatedId("admin");
List<BoardFileEntity> list = fileUtils.parseFileInfo2(boardEntity.getBoardIdx(), request);
if (!CollectionUtils.isEmpty(list)) {
boardEntity.setFileInfoList(list);
}
jpaBoardRepository.save(boardEntity);
}
@Override
public void updateBoard(BoardEntity boardEntity) {
BoardEntity existingBoard = jpaBoardRepository.findById(boardEntity.getBoardIdx()).orElse(null);
if (existingBoard != null) {
boardEntity.setFileInfoList(existingBoard.getFileInfoList());
}
jpaBoardRepository.save(boardEntity);
}
@Override
public void deleteBoard(int boardIdx) {
jpaBoardRepository.deleteById(boardIdx);
}
@Override
public BoardFileEntity selectBoardFileInfo(int idx, int boardIdx) {
return jpaBoardRepository.findBoardFile(boardIdx, boardIdx);
}
}
package board.common;
import java.io.File;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import board.entity.BoardFileEntity;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import board.dto.BoardFileDto;
@Component
public class FileUtils {
@Value("${spring.servlet.multipart.location}")
private String uploadDir;
// 요청을 통해서 전달받은 파일을 저장하고, 파일 정보를 반환하는 메서드
public List<BoardFileDto> parseFileInfo(int boardIdx, MultipartHttpServletRequest request) throws Exception {
if (ObjectUtils.isEmpty(request)) {
return null;
}
List<BoardFileDto> fileInfoList = new ArrayList<>();
// 파일을 저장할 디렉터리를 설정
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMdd");
ZonedDateTime now = ZonedDateTime.now();
String storedDir = uploadDir + "images\\" + now.format(dtf);
File fileDir = new File(storedDir);
if (!fileDir.exists()) {
fileDir.mkdirs();
}
// 업로드 파일 데이터를 디렉터리에 저장하고 정보를 리스트에 저장
Iterator<String> fileTagNames = request.getFileNames();
while(fileTagNames.hasNext()) {
String fileTagName = fileTagNames.next();
List<MultipartFile> files = request.getFiles(fileTagName);
for (MultipartFile file : files) {
String originalFileExtension = "";
// 파일 확장자를 ContentType에 맞춰서 지정
if (!file.isEmpty()) {
String contentType = file.getContentType();
if (ObjectUtils.isEmpty(contentType)) {
break;
} else {
if (contentType.contains("image/jpeg")) {
originalFileExtension = ".jpg";
} else if (contentType.contains("image/png")) {
originalFileExtension = ".png";
} else if (contentType.contains("image/gif")) {
originalFileExtension = ".gif";
} else {
break;
}
}
// 저장에 사용할 파일 이름을 조합
String storedFileName = Long.toString(System.nanoTime()) + originalFileExtension;
String storedFilePath = storedDir + "\\" + storedFileName;
// 파일 정보를 리스트에 저장
BoardFileDto dto = new BoardFileDto();
dto.setBoardIdx(boardIdx);
dto.setFileSize(Long.toString(file.getSize()));
dto.setOriginalFileName(file.getOriginalFilename());
dto.setStoredFilePath(storedFilePath);
fileInfoList.add(dto);
// 파일 저장
fileDir = new File(storedFilePath);
file.transferTo(fileDir);
}
}
}
return fileInfoList;
}
public List<BoardFileEntity> parseFileInfo2(int boardIdx, MultipartHttpServletRequest request) throws Exception {
if (ObjectUtils.isEmpty(request)) {
return null;
}
List<BoardFileEntity> fileInfoList = new ArrayList<>();
// 파일을 저장할 디렉터리를 설정
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMdd");
ZonedDateTime now = ZonedDateTime.now();
String storedDir = uploadDir + "images\\" + now.format(dtf);
File fileDir = new File(storedDir);
if (!fileDir.exists()) {
fileDir.mkdirs();
}
// 업로드 파일 데이터를 디렉터리에 저장하고 정보를 리스트에 저장
Iterator<String> fileTagNames = request.getFileNames();
while(fileTagNames.hasNext()) {
String fileTagName = fileTagNames.next();
List<MultipartFile> files = request.getFiles(fileTagName);
for (MultipartFile file : files) {
String originalFileExtension = "";
// 파일 확장자를 ContentType에 맞춰서 지정
if (!file.isEmpty()) {
String contentType = file.getContentType();
if (ObjectUtils.isEmpty(contentType)) {
break;
} else {
if (contentType.contains("image/jpeg")) {
originalFileExtension = ".jpg";
} else if (contentType.contains("image/png")) {
originalFileExtension = ".png";
} else if (contentType.contains("image/gif")) {
originalFileExtension = ".gif";
} else {
break;
}
}
// 저장에 사용할 파일 이름을 조합
String storedFileName = Long.toString(System.nanoTime()) + originalFileExtension;
String storedFilePath = storedDir + "\\" + storedFileName;
// 파일 정보를 리스트에 저장
BoardFileEntity entity = new BoardFileEntity();
/*
entity.setBoardIdx(boardIdx);
*/
entity.setFileSize(String.valueOf(file.getSize()));
entity.setOriginalFileName(file.getOriginalFilename());
entity.setStoredFilePath(storedFilePath);
fileInfoList.add(entity);
// 파일 저장
fileDir = new File(storedFilePath);
file.transferTo(fileDir);
}
}
}
return fileInfoList;
}
}
JpaBoardController
package board.controller;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;
import board.dto.BoardInsertRequest;
import board.entity.BoardEntity;
import board.entity.BoardFileEntity;
import board.service.JpaBoardService;
import jakarta.servlet.http.HttpServletResponse;
@Controller
@RequestMapping("/jpa")
public class JpaBoardController {
@Autowired
private JpaBoardService boardService;
// 목록 조회
@GetMapping("/board")
public ModelAndView openBoardList() throws Exception {
ModelAndView mv = new ModelAndView("/board/jpaBoardList");
List<BoardEntity> list = boardService.selectBoardList();
mv.addObject("list", list);
return mv;
}
// 작성 화면
@GetMapping("/board/write")
public String openBoardWrite() throws Exception {
return "/board/jpaBoardWrite";
}
// 저장 처리
@PostMapping("/board/write")
public String insertBoard(BoardInsertRequest boardInsertRequest, MultipartHttpServletRequest request) throws Exception {
BoardEntity boardEntity = new ModelMapper().map(boardInsertRequest, BoardEntity.class);
boardService.insertBoard(boardEntity, request);
return "redirect:/jpa/board";
}
// 상세 조회
@GetMapping("/board/{boardIdx}")
public ModelAndView openBoardDetail(@PathVariable("boardIdx") int boardIdx) throws Exception {
BoardEntity boardEntity = boardService.selectBoardDetail(boardIdx);
ModelAndView mv = new ModelAndView("/board/jpaBoardDetail");
mv.addObject("board", boardEntity);
return mv;
}
// 수정 처리
@PutMapping("/board/{boardIdx}")
public String updateBoard(@PathVariable("boardIdx") int boardIdx, BoardEntity boardEntity) throws Exception {
boardEntity.setBoardIdx(boardIdx);
boardService.updateBoard(boardEntity);
return "redirect:/jpa/board";
}
// 삭제 처리
@DeleteMapping("/board/{boardIdx}")
public String deleteBoard(@RequestParam("boardIdx") int boardIdx) throws Exception {
boardService.deleteBoard(boardIdx);
return "redirect:/jpa/board";
}
// 첨부파일 다운로드
@GetMapping("/board/file")
public void downloadBoardFile(@RequestParam("idx") int idx, @RequestParam("boardIdx") int boardIdx, HttpServletResponse response) throws Exception {
BoardFileEntity boardFileEntity = boardService.selectBoardFileInfo(idx, boardIdx);
if (ObjectUtils.isEmpty(boardFileEntity)) {
return;
}
Path path = Paths.get(boardFileEntity.getStoredFilePath());
byte[] file = Files.readAllBytes(path);
response.setContentType("application/octet-stream");
response.setContentLength(file.length);
response.setHeader("Content-Disposition", "attachment; fileName=\"" + URLEncoder.encode(boardFileEntity.getOriginalFileName(), "UTF-8") + "\";");
response.setHeader("Content-Transfer-Encoding", "binary");
response.getOutputStream().write(file);
response.getOutputStream().flush();
response.getOutputStream().close();
}
}
jpaBoardDetail.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>JPA 게시판</title>
<link rel="stylesheet" th:href="@{/css/board.css}" />
</head>
<body>
<div class="container">
<h2>JPA 게시판 상세</h2>
<form id="frm" method="post">
<input type="hidden" id="boardIdx" name="boardIdx"
th:value="${board.boardIdx}" />
<!-- HiddenHttpMethodFilter 에서 요청 방식 결정에 사용할 값을 전달하는 용도 -->
<input type="hidden" id="method" name="_method" />
<table class="board_detail">
<colgroup>
<col width="15%" />
<col width="*" />
<col width="15%" />
<col width="35%" />
</colgroup>
<tr>
<th>글번호</th>
<td th:text="${board.boardIdx}"></td>
<th>조회수</th>
<td th:text="${board.hitCnt}"></td>
</tr>
<tr>
<th>작성자</th>
<td th:text="${board.createdId}"></td>
<th>작성일</th>
<td th:text="${board.createdDt}"></td>
</tr>
<tr>
<th>제목</th>
<td colspan="3"><input type="text" id="title" name="title"
th:value="${board.title}" /></td>
</tr>
<tr>
<td colspan="4"><textarea id="contents" name="contents"
th:text="${board.contents}"></textarea></td>
</tr>
</table>
</form>
<!-- 첨부파일 목록 -->
<div class="file_list">
<a th:each="file : ${board.fileInfoList}"
th:text="|${file.originalFileName} (${file.fileSize}kb)|"
th:href="@{/jpa/board/file(idx=${file.idx}, boardIdx=${file.boardIdx})}"></a>
</div>
<input type="button" id="list" class="btn" value="목록으로" /> <input
type="button" id="update" class="btn" value="수정하기" /> <input
type="button" id="delete" class="btn" value="삭제하기" />
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
$(function() {
const boardIdx = $("#boardIdx").val();
$("#list").on("click", function() {
location.href = "/jpa/board";
});
$("#update").on("click", function() {
$("#method").val("PUT");
let frm = $("#frm")[0];
frm.action = "/jpa/board/" + boardIdx;
frm.submit();
});
$("#delete").on("click", function() {
$("#method").val("DELETE");
let frm = $("#frm")[0];
frm.action = "/jpa/board/" + boardIdx;
frm.submit();
});
});
</script>
</div>
</body>
</html>
jpaBoardList.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>JPA 게시판</title>
<link rel="stylesheet" th:href="@{/css/board.css}" />
</head>
<body>
<div class="container">
<h2>JPA 게시판 목록</h2>
<table class="board_list">
<colgroup>
<col width="15%" />
<col width="*" />
<col width="15%" />
<col width="20%" />
</colgroup>
<thead>
<tr>
<th scope="col">글번호</th>
<th scope="col">제목</th>
<th scope="col">조회수</th>
<th scope="col">작성일</th>
</tr>
</thead>
<tbody>
<tr th:if="${#lists.size(list)} > 0" th:each="list : ${list}">
<td th:text="${list.boardIdx}"></td>
<td class="title">
<a href="/jpa/board/" th:attrappend="href=${list.boardIdx}" th:text="${list.title}"></a>
</td>
<td th:text="${list.hitCnt}"></td>
<td th:text="${list.createdDt}"></td>
</tr>
<tr th:unless="${#lists.size(list)} > 0">
<td colspan="4">조회된 결과가 없습니다.</td>
</tr>
</tbody>
</table>
<a href="/jpa/board/write" class="btn">글쓰기</a>
</div>
</body>
</html>
jpaBoardWrite.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>JPA 게시판</title>
<link rel="stylesheet" th:href="@{/css/board.css}" />
</head>
<body>
<div class="container">
<h2>JPA 게시판 등록</h2>
<form id="frm" method="post" action="/jpa/board/write" enctype="multipart/form-data">
<table class="board_detail">
<tr>
<td>제목</td>
<td><input type="text" id="title" name="title" /></td>
</tr>
<tr>
<td colspan="2"><textarea id="contents" name="contents"></textarea></td>
</tr>
</table>
<input type="file" id="files" name="files" multiple="multiple" />
<input type="submit" id="submit" value="저장" class="btn" />
</form>
</div>
</body>
</html>
spring.jpa.hibernate.ddl-auto=update
http://localhost:8080/jpa/board
<S extends T> S save(S entity) | 주어진 엔티티를 저장(업데이트 포함) |
<S extends T> Iterable<S> saveAll(Iterable<S> entities) | 주어진 엔티티 목록을 저장 |
Optional<T> findById(ID id) | 주어진 ID로 식별된 엔티티를 반환 |
existsById(ID id) | 주어진 ID로 식별된 엔티티의 존재 여부를 반환 |
Iterable<T> findAll() | 모든 엔티티를 반환 |
Iterable<T> findAllById(Iterable<ID> ids) | 주어진 ID 목록에 맞는 모든 엔티티 목록을 반환 |
long count() | 사용 가능한 엔티티의 개수를 반환 |
void deleteById(ID id) | 주어진 ID로 식별된 엔티티를 삭제 |
void delete(T entity) | 주어진 엔티티를 삭제 |
void deleteAllById(Iterable<? extends ID> ids) | 주어진 ID 목록으로 식별된 모든 엔티티를 삭제 |
void deleteAll(Iterable<? extends T> entities) | 주어진 엔티티 목록으로 식별된 엔티티를 모두 삭제 |
void deleteAll() | 모든 엔티티를 삭제 |
Iterable<T> findAll(Sort sort)
Page<T> findAll(Pageable pageable)
위 인터페이스를 활용
https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html
접두사 + By + 필드이름 + 조건 + 정렬
~~~~~~ ~~ ~~~~~~~~ ~~~~ ~~~~
| | | | |
| | | | +-- OrderBy를 사용해 정렬 조건을 지정
| | | +-- Equals, Like, GreaterThan, Between 등
| | +-- 엔티티의 필드 이름
| +-- 필드 이름과 조건을 지정하기 전에 사용
+-- find, read, get, query, count, delete
findByTitle(String title)
// 쿼리 메서드
=> select jpaboard0.board_idx as board_id1_0, ... from tjpa_board jpaboard0 where jpaboard0_.ttitle = ? // JPQL
List<User> findByLastName(String lastName);
=> lastName 필드가 주어진 값과 일치하는 모든 User 엔티티를 조회
=> select u from User u where u.lastName = ?1 이런 쿼리가 자동으로 나간다.
List<User> findByLastNameAndFirstName(String lastName, String firstName);
=> lastName 필드와 firstName 필드가 모두 일치하는 User 엔티티를 조회
=> select u from User u where u.lastName = ?1 and u.firstName = ?2
List<User> findByFirstNameLike(String firstName);
=> select u from User u where u.firstName like ?1
List<User> findByFirstNameOrderByFirstNameAsc(String firstName);
=> select u from User u where u.firstName = ?1 order by u.firstName asc
long countByLastName(String lastName);
=> select count(u) from User u where u.lastName = ?1
void deleteByLastName(String lastName);
=> delete from User u where u.lastName = ?1
List<User> findByAgeBetween(int startAge, int endAge);
=> select u from User u where u.age between ?1 and ?2
List<User> findByLastNameIn(Collection<String> lastName);
=> select u from User u where u.lastName in ?1
List<User> findByLastNameIsNull();
=> select u from User u where u.lastName is null
메서드 이름이 복잡하거나 쿼리 메서드로 표현하기 힘든 경우 ⇒ @Query 어노테이션으로 쿼리를 직접 작성
but, 가급적이면 쓰지 말자. 적용이 안 될 수도 있고 쿼리 메서드의 목적인 추상화에 걸맞지 않음
JPQL(Java Persistence Query Language) 또는 데이터베이스에 맞는 네이티브 SQL 사용이 가능
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
FROM 절에 테이블 이름이 아닌 엔티티 이름을 사용
// 순번으로 제어
@Query("select file from BoardFileEntity file where boardIdx = ?1 and idx = ?2")
BoardFileEntity findBoardFile(int boardIdx, int idx);
// 이름으로 제어
@Query("select file from BoardFileEntity file where boardIdx = :boardIdx and idx = :idx")
BoardFileEntity findBoardFile(@Param("boardIdx") int boardIdx, @Param("idx") int idx);
RestApiBoardController를 복사해서 JpaRestApiBoardController 만들고 내용을 수정하고 BoartDto를 BoardEntity로 변경
JpaRestApiBoardController
package board.controller;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import board.dto.BoardListResponse;
import board.entity.BoardEntity;
import board.entity.BoardFileEntity;
import board.service.JpaBoardService;
import jakarta.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/api/v2")
public class JpaRestApiBoardController {
@Autowired
private JpaBoardService boardService;
// 목록 조회
@GetMapping("/board")
public ResponseEntity<Object> openBoardList() throws Exception {
List<BoardListResponse> results = new ArrayList<>();
try {
List<BoardEntity> boardList = boardService.selectBoardList();
boardList.forEach(dto -> results.add(new ModelMapper().map(dto, BoardListResponse.class)));
return new ResponseEntity<>(results, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>("목록 조회 실패", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// 저장 처리
@PostMapping(value = "/board", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Object> insertBoard(@RequestParam("board") String boardData, MultipartHttpServletRequest request) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
BoardEntity boardDto = objectMapper.readValue(boardData, BoardEntity.class);
Map<String, String> result = new HashMap<>();
try {
boardService.insertBoard(boardDto, request);
result.put("message", "게시판 저장 성공");
return ResponseEntity.status(HttpStatus.CREATED).body(result);
} catch(Exception e) {
result.put("message", "게시판 저장 실패");
result.put("description", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
// 상세 조회
@GetMapping("/board/{boardIdx}")
public ResponseEntity<Object> openBoardDetail(@PathVariable("boardIdx") int boardIdx) throws Exception {
BoardEntity boardEntity = boardService.selectBoardDetail(boardIdx);
if (boardEntity == null) {
Map<String, Object> result = new HashMap<>();
result.put("code", HttpStatus.NOT_FOUND.value());
result.put("name", HttpStatus.NOT_FOUND.name());
result.put("message", "게시판 번호 " + boardIdx + "와 일치하는 게시물이 존재하지 않습니다.");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(result);
} else {
return ResponseEntity.status(HttpStatus.OK).body(boardEntity);
}
}
// 수정 처리
@PutMapping("/board/{boardIdx}")
public void updateBoard(@PathVariable("boardIdx") int boardIdx, @RequestBody BoardEntity boardEntity) throws Exception {
boardEntity.setBoardIdx(boardIdx);
boardService.updateBoard(boardEntity);
}
// 삭제 처리
@DeleteMapping("/board/{boardIdx}")
public void deleteBoard(@PathVariable("boardIdx") int boardIdx) throws Exception {
boardService.deleteBoard(boardIdx);
}
// 첨부파일 다운로드
@GetMapping("/board/file")
public void downloadBoardFile(@RequestParam("idx") int idx, @RequestParam("boardIdx") int boardIdx, HttpServletResponse response) throws Exception {
BoardFileEntity boardFileEntity = boardService.selectBoardFileInfo(idx, boardIdx);
if (ObjectUtils.isEmpty(boardFileEntity)) {
return;
}
Path path = Paths.get(boardFileEntity.getStoredFilePath());
byte[] file = Files.readAllBytes(path);
response.setContentType("application/octet-stream");
response.setContentLength(file.length);
response.setHeader("Content-Disposition", "attachment; fileName=\"" + URLEncoder.encode(boardFileEntity.getOriginalFileName(), "UTF-8") + "\";");
response.setHeader("Content-Transfer-Encoding", "binary");
response.getOutputStream().write(file);
response.getOutputStream().flush();
response.getOutputStream().close();
}
}
이전 프로젝트 결과물을 JPA 기반의 REST API로 변경해 보세요.