본문 바로가기
portfolio/게시판 만들기

게시판 crud 작업

by LeeGangEun 2023. 7. 10.

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의 변경감지로 조회수를 증가시켜주다보니
동시에 여러 사용자가 누르면 정상적인 조회수 증가 처리를 못해준다는 것이다.

추후 이 문제를 보완해보도록 하겠다.