Entity
모든 테이블을 생성하지 않고 우선 게시판 CRUD를 위해 Board 테이블만 생성하겠다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@AllArgsConstructor
@Builder
@Entity
@Table(name = "board")
public class Board extends BaseTime {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "board_id")
private Long id;
@Column(nullable = false)
private String title;
@Lob
@Column(nullable = false)
private String content;
@Column(nullable = false)
private String writer;
@Column(nullable = false)
@ColumnDefault("0")
private int views;
public void update(String title, String content) {
this.title = title;
this.content = content;
}
public void incrementViewCount() {
this.views++;
}
}
@EntityListeners(AuditingEntityListener.class)
@Getter
@MappedSuperclass
public class BaseTime {
@CreatedDate
@Column(name = "create_date", nullable = false, updatable = false)
private LocalDateTime createDate;
@LastModifiedDate
@Column(name = "modified_date", nullable = false)
private LocalDateTime modifiedDate;
}
DTO
package com.devblog.domain.dto;
import com.devblog.domain.entity.Board;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
public class BoardDTO {
@Getter
@AllArgsConstructor
@Builder
public static class Request {
private String title;
private String content;
private String writer;
public Board toEntity() {
return Board.builder()
.title(title)
.content(content)
.writer(writer)
.build();
}
}
@Getter
@AllArgsConstructor
@Builder
public static class Response {
private Long id;
private String title;
private String content;
private String writer;
private int views;
public Response (Board board) {
this.id = board.getId();
this.title = board.getTitle();
this.content = board.getContent();
this.writer = board.getWriter();
this.views = board.getViews();
}
}
}
Service
package com.devblog.service;
import com.devblog.domain.dto.BoardDTO;
import com.devblog.domain.entity.Board;
import com.devblog.domain.repository.BoardRepository;
import com.devblog.exception.CustomException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static com.devblog.exception.ErrorCode.*;
@Slf4j
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
private static final String BOARD_COOKIE_NAME = "board_cookie";
public List<BoardDTO.Response> findAll() {
List<Board> boardList = boardRepository.findAll();
return boardList.stream().map(BoardDTO.Response::new).collect(Collectors.toList());
}
public Long save(BoardDTO.Request boardDTO) {
Board board = boardRepository.save(boardDTO.toEntity());
return board.getId();
}
public BoardDTO.Response findById(Long id) {
Board board = boardRepository.findById(id)
.orElseThrow(() -> new CustomException(BOARD_NOT_FOUND));
board.incrementViewCount();
return new BoardDTO.Response(board);
}
public void update(Long id, BoardDTO.Request dto) {
Board board = boardRepository.findById(id)
.orElseThrow(() -> new CustomException(BOARD_NOT_FOUND));
board.update(dto.getTitle(), dto.getContent());
}
public void delete(Long id) {
Board board = boardRepository.findById(id)
.orElseThrow(() -> new CustomException(BOARD_NOT_FOUND));
boardRepository.delete(board);
}
public void incrementView(Long id, HttpServletRequest request, HttpServletResponse response) {
Board board = boardRepository.findById(id)
.orElseThrow(() -> new CustomException(BOARD_NOT_FOUND));
Cookie[] cookies = request.getCookies();
if (cookies == null || Arrays.stream(cookies).noneMatch(cookie -> cookie.getName().equals(BOARD_COOKIE_NAME + id))) {
board.incrementViewCount();
Cookie newCookie = createCookieForForNotOverlap(id);
response.addCookie(newCookie);
boardRepository.save(board);
}
}
private Cookie createCookieForForNotOverlap(Long id) {
Cookie cookie = new Cookie(BOARD_COOKIE_NAME + id, String.valueOf(id));
cookie.setComment("duplicate views prevention");
cookie.setMaxAge(getRemainSecondForTomorrow()); // 쿠키 유지기간 하루
cookie.setHttpOnly(true); // 서버에서만 조작 가능
return cookie;
}
private int getRemainSecondForTomorrow() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime tomorrow = LocalDateTime.now().plusDays(1L).truncatedTo(ChronoUnit.DAYS);
return (int) now.until(tomorrow, ChronoUnit.SECONDS);
}
}
Controller
package com.devblog.controller;
import com.devblog.domain.dto.BoardDTO;
import com.devblog.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.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
@Controller
public class BoardController {
private final BoardService boardService;
@GetMapping({"board", "", "/"})
public String board(Model model) {
List<BoardDTO.Response> boardList = boardService.findAll();
model.addAttribute("dtoList", boardList);
return "board";
}
@GetMapping("register")
public void register() {
}
@GetMapping("modify/{id}")
public String modify(@PathVariable Long id, Model model) {
BoardDTO.Response dto = boardService.findById(id);
model.addAttribute("dto", dto);
return "modify";
}
@GetMapping("read/{id}")
public String read(@PathVariable Long id, Model model, HttpServletRequest request, HttpServletResponse response) {
boardService.incrementView(id, request, response);
BoardDTO.Response dto = boardService.findById(id);
model.addAttribute("dto", dto);
return "read";
}
@PostMapping("register")
public void register(BoardDTO.Request requestDTO) {
boardService.save(requestDTO);
}
@PutMapping("modify/{id}")
public void modify(@PathVariable Long id, BoardDTO.Request request) {
boardService.update(id, request);
}
@DeleteMapping("delete/{id}")
public void delete(@PathVariable Long id) {
boardService.delete(id);
}
}
이제 crud가 정상적으로 되는지 postman으로 테스트 해보자
등록 테스트
정상적으로 값이 저장된걸 확인할 수 있다.
수정 테스트
수정도 정상적으로 된 걸 확인할 수 있다.
삭제 테스트
삭제도 정상적으로 된 걸 확인할 수 있다.
단건 조회 테스트
조회할때는 조회수도 증가하는지 확인해봐야한다.
조회수는 어뷰징 방지를 위해 쿠키로 관리할 수 있게 했고,
조회수 증가 로직을 천천히 확인해보자.
먼저 read 요청시 HttpServeletRequest , HttpServletResponse를 매개변수로 받아 서비스에 넘거줬다.
그 후
조회시 해당 게시물을 하루내에 조회한 적이 없으면 조회수가 증가하게 로직을 구현해줬다.
하지만 여기엔 허점이 있다..
JPA의 변경감지로 조회수를 증가시켜주다보니
동시에 여러 사용자가 누르면 정상적인 조회수 증가 처리를 못해준다는 것이다.
추후 이 문제를 보완해보도록 하겠다.
'portfolio > 게시판 만들기' 카테고리의 다른 글
Pageable 이용한 페이징 (0) | 2023.08.13 |
---|---|
Jasypt를 사용하여 properties(yml) 주요 정보 암호화 (0) | 2023.07.09 |
프로젝트 설계 (0) | 2023.07.09 |