토이프로젝트

[팀프로젝트]모바일앱 게시판 만들기 - 3편 : 게시판 CRUD 개발하기

  • -
반응형

이번 글부터 게시판 개발 내용을 진행합니다. 

현재까지 추가된 API는 다음과 같습니다.

  • [POST] /oauth/kakao : 로그인
  • [POST] /user/join : 회원가입
  • [GET] /user/profile/{userId} : 사용자 정보 조회

 

포스팅 내용이 아래 순서대로 진행되고 있으니 이전 글을 확인해 주세요.

  1. 테이블 설계하기
  2. 로그인/회원가입 개발하기
    1. 로그인 API 추가
    2. 회원가입 API 추가
  3. 게시판 CRUD 개발하기

1. 테이블 생성

게시판 기능에 연관된 테이블을 생성하겠습니다. 실제 앱에는 아래 테이블외에 추가된 내용이 더 있습니다. 

여기서는 게시판, 게시판 댓글, 게시판 좋아요와 관련된 내용만 소개해보겠습니다.

모든 테이블은 회원 번호(USER_NO)를 통해 회원(USERS) 테이블과 연결됩니다.

 

댓글 테이블은 대댓글 기능 구현을 위해 부모-자식 관계(COMMENT_SEQ - PARENT_SEQ)로 이루어져있습니다.  PARENT_SEQ가 0이면 루트르 고정되며, 0이 아닌 다른 값이 온다면 자식 댓글로 인식합니다.

단순화를 위해 대댓글의 깊이를 1까지만 허용했습니다.

 

CREATE TABLE `BOARD` (
  `BOARD_SEQ` bigint NOT NULL AUTO_INCREMENT COMMENT 'BOARD_SEQ',
  `BOARD_TITLE` varchar(30) NOT NULL COMMENT '제목',
  `BOARD_CONTENT` text COMMENT '내용',
  `MARKET_CODE` varchar(10) DEFAULT NULL COMMENT '도매시장코드',
  `COMPANY_CODE` varchar(10) DEFAULT NULL COMMENT '법인코드',
  `USER_NO` bigint DEFAULT NULL COMMENT '회원번호',
  `CREATE_DATE` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '게시일',
  PRIMARY KEY (`BOARD_SEQ`)
) COMMENT='게시판';

CREATE TABLE `BOARD_COMMENTS` (
  `COMMENT_SEQ` bigint NOT NULL AUTO_INCREMENT COMMENT 'COMMENT_SEQ',
  `BOARD_SEQ` bigint NOT NULL COMMENT 'BOARD_SEQ',
  `PARENT_SEQ` bigint DEFAULT NULL COMMENT '부모COMMENT_SEQ',
  `USER_NO` bigint NOT NULL COMMENT '회원번호',
  `COMMENT_CONTENT` text NOT NULL COMMENT '내용',
  `CREATE_DATE` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '게시일',
  PRIMARY KEY (`COMMENT_SEQ`),
  KEY `board_comments_USER_ID_IDX` (`USER_NO`) USING BTREE,
  KEY `BOARD_COMMENTS_BOARD_SEQ_IDX` (`BOARD_SEQ`) USING BTREE
) COMMENT='게시판 댓글';

CREATE TABLE `BOARD_LIKES` (
  `LIKE_SEQ` bigint NOT NULL AUTO_INCREMENT COMMENT 'LIKE_SEQ',
  `BOARD_SEQ` bigint NOT NULL COMMENT 'BOARD_SEQ',
  `USER_NO` bigint NOT NULL COMMENT '아이디',
  `CREATE_DATE` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '게시일',
  PRIMARY KEY (`LIKE_SEQ`),
  KEY `board_likes_BOARD_SEQ_IDX` (`BOARD_SEQ`,`USER_NO`) USING BTREE
) COMMENT='게시판 좋아요';

 

 

2. 게시판 API 만들기

2-1. Entity, Dto, Mapper 추가

테이블을 토대로 Entity, Dto 클래스를 생성해보겠습니다. 이전 글에서 언급했듯 Dto와 Entity 매핑을 위해서 MapStruct를 사용합니다.

테이블의 PK는 자동 생성 전략을 사용했습니다.

 

[Entity]

@Getter
@Entity
@Builder(toBuilder = true)
@Table(name = "BOARD")
@AllArgsConstructor
@NoArgsConstructor
public class Board {

    @Id
    @Column(name = "BOARD_SEQ")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long boardSeq;
    @Column(name = "BOARD_TITLE")
    private String boardTitle;
    @Column(name = "BOARD_CONTENT")
    private String boardContent;
    @Column(name = "MARKET_CODE")
    private String marketCode;
    @Column(name = "COMPANY_CODE")
    private String companyCode;
    @Column(name = "USER_NO")
    private Long userNo;
    @Column(name = "CREATE_DATE")
    @UpdateTimestamp
    private LocalDateTime createDate;

}
@Getter
@Entity
@Builder(toBuilder = true)
@Table(name = "BOARD_COMMENTS")
@AllArgsConstructor
@NoArgsConstructor
public class BoardComments {

    @Id
    @Column(name = "COMMENT_SEQ")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long commentSeq;
    @Column(name = "BOARD_SEQ")
    private Long boardSeq;
    @Column(name = "PARENT_SEQ")
    private Long parentSeq;
    @Column(name = "USER_NO")
    private Long userNo;
    @Column(name = "COMMENT_CONTENT")
    private String commentContent;
    @Column(name = "CREATE_DATE")
    @UpdateTimestamp
    private LocalDateTime createDate;

}
@Getter
@Entity
@Builder(toBuilder = true)
@Table(name = "BOARD_LIKES")
@AllArgsConstructor
@NoArgsConstructor
public class BoardLikes {

    @Id
    @Column(name = "LIKE_SEQ")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long likeSeq;
    @Column(name = "BOARD_SEQ")
    private Long boardSeq;
    @Column(name = "USER_NO")
    private Long userNo;
    @Column(name = "CREATE_DATE")
    @UpdateTimestamp
    private LocalDateTime createDate;

}

 

[Dto]

@Data
public class BoardDto {

    private Long boardSeq;
    private String boardTitle;
    private String boardContent;
    private String marketCode;
    private String companyCode;
    private Long userNo;

}
@Data
@Builder
public class CommentsDto {

    private Long commentSeq;
    private Long boardSeq;
    private Long parentSeq;
    private Long userNo;
    private String userId;
    private String nickName;
    private String marketName;
    private String companyName;
    private String profileImageUrl;
    private String commentContent;
    private String createDate;
    private List<CommentsDto> subCommentsList;

}
@Data
public class LikesDto {

    private Long boardSeq;
    private Long userNo;
}

 

[Mapper]

@Mapper(componentModel = "spring")
public interface BoardMapper {

    BoardDto toBoardDto(Board entity);

    Board toBoardEntity(BoardDto dto);

    LikesDto toLikesDto(BoardLikes entity);

    BoardLikes toLikesEntity(LikesDto dto);

    CommentsDto toCommentsDto(BoardComments entity);

    BoardComments toCommentsEntity(CommentsDto dto);
}

 

2-2. Repository 추가

테이블 조인이 많이 사용되는 경우 편의를 위해 네이티브 쿼리를 이용했습니다.

 

BoardRepository : 게시판

  • countByBoardTitleLike : 제목 검색시 게시글 개수
  • countByBoardContentLike : 내용 검색시 게시글 개수
  • getBoardList : 게시글 리스트 조회
  • getBoardInfo : 게시글 내용 조회
@Repository
public interface BoardRepository extends JpaRepository<Board, Long> {

    int countByBoardTitleLike(String boardTitle);

    int countByBoardContentLike(String boardContent);

    @Query(value = ""
        + "SELECT MAX(b1.BOARD_SEQ) AS boardSeq \n"
        + "\t , MAX(b1.BOARD_TITLE) AS boardTitle \n"
        + "\t , LEFT(MAX(b1.BOARD_CONTENT), 50) AS boardContent \n"
        + "\t , MAX(b1.MARKET_CODE) AS marketCode \n"
        + "\t , MAX(b1.COMPANY_CODE) AS companyCode \n"
        + "\t , MAX(u1.USER_ID) AS userId \n"
        + "\t , MAX(u1.NICK_NAME) AS nickName \n"
        + "\t , MAX(m1.MARKET_NAME) AS marketName \n"
        + "\t , MAX(c1.COMPANY_NAME) AS companyName \n"
        + "\t , MAX(u1.PROFILE_IMAGE_URL) AS profileImageUrl \n"
        + "\t , IFNULL(b2.LIKE_COUNT, 0) AS likeCount \n"
        + "\t , IFNULL(b3.COMMENT_COUNT, 0) AS commentCount\n"
        + "\t , 0 AS isLiked \n"
        + "\t , DATE_FORMAT(MAX(b1.CREATE_DATE), '%Y%m%d%H%i') AS createDate \n"
        + "  FROM BOARD b1 \n"
        + "  \t   INNER JOIN USERS u1 \n"
        + "  \t     ON b1.USER_NO = u1.USER_NO \n"
        + "  \t   LEFT OUTER JOIN MARKETS m1 \n"
        + "  \t     ON u1.MARKET_CODE = m1.MARKET_CODE \n"
        + "  \t   LEFT OUTER JOIN COMPANIES c1 \n"
        + "  \t     ON u1.COMPANY_CODE = c1.COMPANY_CODE\t \n"
        + "  \t   LEFT OUTER JOIN \n"
        + "  \t   (\n"
        + "  \t   SELECT bl.BOARD_SEQ, COUNT(bl.LIKE_SEQ) AS LIKE_COUNT \n"
        + "  \t     FROM BOARD_LIKES bl \n"
        + "  \t          INNER JOIN USERS u \n"
        + "  \t             ON bl.USER_NO = u.USER_NO \n"
        + "  \t    GROUP BY bl.BOARD_SEQ \n"
        + "  \t   ) b2\n"
        + "  \t   ON b1.BOARD_SEQ = b2.BOARD_SEQ \n"
        + "  \t   LEFT OUTER JOIN \n"
        + "  \t   (\n"
        + "  \t   SELECT bc.BOARD_SEQ, COUNT(bc.COMMENT_SEQ) AS COMMENT_COUNT \n"
        + "  \t     FROM BOARD_COMMENTS bc \n"
        + "  \t          INNER JOIN USERS u \n"
        + "  \t             ON bc.USER_NO = u.USER_NO \n"
        + "  \t    GROUP BY bc.BOARD_SEQ \n"
        + "  \t   ) b3\n"
        + "  \t   ON b1.BOARD_SEQ = b3.BOARD_SEQ"
        + " WHERE CASE WHEN :search = 'title' THEN b1.BOARD_TITLE LIKE %:text% \n"
        + "        \t  WHEN :search = 'content' THEN b1.BOARD_CONTENT LIKE %:text% \n"
        + "        \t  ELSE :search = ''\n"
        + "        END  "
        + " GROUP BY b1.BOARD_SEQ\n"
        + " ORDER BY b1.BOARD_SEQ DESC\n"
        + " LIMIT 10 OFFSET :offset", nativeQuery = true)
    List<BoardInfo> getBoardList(
        @Param("offset") int offset,
        @Param("search") String search,
        @Param("text") String text);


    @Query(value = ""
        + "SELECT MAX(b1.BOARD_SEQ) AS boardSeq \n"
        + "\t , MAX(b1.BOARD_TITLE) AS boardTitle \n"
        + "\t , MAX(b1.BOARD_CONTENT) AS boardContent \n"
        + "\t , MAX(b1.MARKET_CODE) AS marketCode \n"
        + "\t , MAX(b1.COMPANY_CODE) AS companyCode \n"
        + "\t , MAX(u1.USER_ID) AS userId \n"
        + "\t , MAX(u1.NICK_NAME) AS nickName \n"
        + "\t , MAX(u1.PROFILE_IMAGE_URL) AS profileImageUrl \n"
        + "\t , MAX(m1.MARKET_NAME) AS marketName \n"
        + "\t , MAX(c1.COMPANY_NAME) AS companyName \n"
        + "\t , IFNULL(b2.LIKE_COUNT, 0) AS likeCount \n"
        + "\t , IFNULL(b3.COMMENT_COUNT, 0) AS commentCount \n"
        + "\t , COUNT(b4.LIKE_SEQ) AS isLiked \n"
        + "\t , DATE_FORMAT(MAX(b1.CREATE_DATE), '%Y%m%d%H%i') AS createDate \t\t\n"
        + "  FROM BOARD b1\n"
        + "  \t   INNER JOIN USERS u1 \n"
        + "  \t     ON b1.USER_NO = u1.USER_NO \n"
        + "  \t   LEFT OUTER JOIN MARKETS m1 \n"
        + "  \t   \t ON u1.MARKET_CODE = m1.MARKET_CODE \n"
        + "  \t   LEFT OUTER JOIN COMPANIES c1 \n"
        + "  \t   \t ON u1.COMPANY_CODE = c1.COMPANY_CODE\t \n"
        + "  \t   LEFT OUTER JOIN \n"
        + "  \t   (\n"
        + "  \t   SELECT bl.BOARD_SEQ, COUNT(bl.LIKE_SEQ) AS LIKE_COUNT \n"
        + "  \t     FROM BOARD_LIKES bl \n"
        + "  \t          INNER JOIN USERS u \n"
        + "  \t             ON bl.USER_NO = u.USER_NO \n"
        + "  \t    GROUP BY bl.BOARD_SEQ \n"
        + "  \t   ) b2\n"
        + "  \t   ON b1.BOARD_SEQ = b2.BOARD_SEQ \n"
        + "  \t   LEFT OUTER JOIN \n"
        + "  \t   (\n"
        + "  \t   SELECT bc.BOARD_SEQ, COUNT(bc.COMMENT_SEQ) AS COMMENT_COUNT \n"
        + "  \t     FROM BOARD_COMMENTS bc \n"
        + "  \t          INNER JOIN USERS u \n"
        + "  \t             ON bc.USER_NO = u.USER_NO \n"
        + "  \t    GROUP BY bc.BOARD_SEQ \n"
        + "  \t   ) b3\n"
        + "  \t   ON b1.BOARD_SEQ = b3.BOARD_SEQ"
        + "  \t   LEFT OUTER JOIN BOARD_LIKES b4 \n"
        + "  \t     ON b1.BOARD_SEQ = b4.BOARD_SEQ \n"
        + "  \t    AND b4.USER_NO   = :userNo \n"
        + "  WHERE b1.BOARD_SEQ = :seq \n"
        + "  GROUP BY b1.BOARD_SEQ ", nativeQuery = true)
    BoardInfo getBoardInfo(@Param("seq") Long seq, @Param("userNo") Long userNo);


}

 

BoardCommentsRepository : 댓글

  • getCommentsList : 댓글 리스트 조회(댓글에 페이징은 적용되지 않았습니다)
  • deleteByBoardSeq : 게시글 삭제시 댓글 삭제
  • deleteByParentSeq : 댓글 삭제시 자식 댓글 삭제
@Repository
public interface BoardCommentsRepository extends JpaRepository<BoardComments, Long> {

    @Query(value = ""
        + "SELECT b1.COMMENT_SEQ AS commentSeq \n"
        + " \t  , b1.BOARD_SEQ AS boardSeq \n"
        + " \t  , b1.PARENT_SEQ AS parentSeq \n"
        + " \t  , u1.USER_ID AS userId \n"
        + " \t  , u1.NICK_NAME AS nickName \n"
        + "\t   , m1.MARKET_NAME AS marketName \n"
        + "\t   , c1.COMPANY_NAME AS companyName \n"
        + " \t  , u1.PROFILE_IMAGE_URL AS profileImageUrl \n"
        + " \t  , b1.COMMENT_CONTENT AS commentContent \n"
        + " \t  , DATE_FORMAT(b1.CREATE_DATE, '%Y%m%d%H%i') AS createDate \n"
        + "   FROM BOARD_COMMENTS b1\n"
        + "   \t    INNER JOIN USERS u1\n"
        + "   \t       ON b1.USER_NO = u1.USER_NO \n"
        + "   \t    LEFT OUTER JOIN MARKETS m1 \n"
        + "   \t      ON u1.MARKET_CODE = m1.MARKET_CODE \n"
        + "   \t    LEFT OUTER JOIN COMPANIES c1 \n"
        + "   \t      ON u1.COMPANY_CODE = c1.COMPANY_CODE\t \n"
        + "   \t    LEFT OUTER JOIN BOARD_COMMENTS b2 \n"
        + "   \t      ON b1.PARENT_SEQ = b2.BOARD_SEQ \n"
        + "  WHERE b1.BOARD_SEQ = :boardSeq \n"
        + "  ORDER BY b1.COMMENT_SEQ DESC", nativeQuery = true)
    List<CommentsInfo> getCommentsList(@Param("boardSeq") Long boardSeq);

    void deleteByBoardSeq(Long seq);

    void deleteByParentSeq(Long seq);

}

 

BoardCommentsRepository : 좋아요

  • findByBoardSeqAndUserNo : 게시글에 사용자가 좋아요를 한 이력이 있는지 체크
  • deleteByBoardSeq : 게시글 삭제시 연관 좋아요 데이터 삭제
@Repository
public interface BoardLikesRepository extends JpaRepository<BoardLikes, Long> {

    BoardLikes findByBoardSeqAndUserNo(Long boardSeq, Long userNo);

    void deleteByBoardSeq(Long seq);
}

 

2-3. Controller 추가

@RestController
@RequestMapping("board")
@RequiredArgsConstructor
public class BoardController {

    private final BoardService boardService;

    /**
     * 게시판 리스트 조회
     *
     * @param page
     * @param search
     * @param text
     * @return
     */
    @GetMapping(value = "/list")
    public ResponseEntity<MessageDto> getBoardList(
        @RequestParam(value = "page", required = false, defaultValue = "1") int page,
        @RequestParam(value = "search", required = false, defaultValue = "") String search,
        @RequestParam(value = "text", required = false, defaultValue = "") String text
    ) {
        Map<String, Object> resultMap = boardService.getBoardList(page, search, text);
        return ResponseEntity.ok().body(new MessageDto("success", resultMap));
    }

    /**
     * 게시글 내용 조회
     *
     * @param seq
     * @return
     */
    @GetMapping(value = "/info")
    public ResponseEntity<MessageDto> getBoardInfo(
        @RequestParam(value = "seq") Long seq
    ) {
        BoardInfo boardInfo = boardService.getBoardInfo(seq);
        return ResponseEntity.ok().body(new MessageDto("success", boardInfo));
    }

    /**
     * 게시글 저장
     *
     * @param boardDto
     * @return
     */
    @PostMapping(value = "/save")
    public ResponseEntity<MessageDto> saveBoard(
        @RequestBody BoardDto boardDto
    ) {
        Board board = boardService.saveBoard(boardDto);
        return ResponseEntity.ok().body(new MessageDto("success", ResponseDto.builder()
            .id(board.getBoardSeq())
            .createdDate(board.getCreateDate()).build()
        ));
    }

    /**
     * 게시글 수정
     *
     * @param seq
     * @param boardDto
     * @return
     * @throws Exception
     */
    @PutMapping(value = "/save/{seq}")
    public ResponseEntity<MessageDto> updateBoard(
        @PathVariable("seq") Long seq,
        @RequestBody BoardDto boardDto
    ) throws Exception {
        Board board = boardService.updateBoard(seq, boardDto);
        return ResponseEntity.ok().body(new MessageDto("success", ResponseDto.builder()
            .id(board.getBoardSeq())
            .createdDate(board.getCreateDate()).build()
        ));
    }

    /**
     * 게시글 삭제
     *
     * @param seq
     * @return
     * @throws Exception
     */
    @DeleteMapping(value = "/item/{seq}")
    public ResponseEntity<MessageDto> deleteBoard(
        @PathVariable("seq") Long seq
    ) throws Exception {
        boardService.deleteBoard(seq);
        return ResponseEntity.ok().body(new MessageDto("success", seq));
    }

    /**
     * 좋아요 등록
     *
     * @param likesDto
     * @return
     */
    @PostMapping(value = "/like")
    public ResponseEntity<MessageDto> saveLike(
        @RequestBody LikesDto likesDto
    ) {
        boardService.saveLikes(likesDto);
        return ResponseEntity.ok().body(new MessageDto("success", likesDto));
    }

    /**
     * 게시판 댓글 리스트 조회
     *
     * @param seq
     * @return
     */
    @GetMapping(value = "/comment/list")
    public ResponseEntity<MessageDto> getCommentsList(
        @RequestParam(value = "seq") Long seq
    ) {
        List<CommentsDto> commentsList = boardService.getCommentsList(seq);
        return ResponseEntity.ok().body(new MessageDto("success", commentsList));
    }

    /**
     * 댓글 저장
     *
     * @param commentDto
     * @return
     */
    @PostMapping(value = "/comment")
    public ResponseEntity<MessageDto> saveComment(
        @RequestBody CommentsDto commentDto
    ) {
        BoardComments comments = boardService.saveComment(commentDto);
        return ResponseEntity.ok()
            .body(new MessageDto("success", ResponseDto.builder()
                .id(comments.getCommentSeq())
                .createdDate(comments.getCreateDate()).build()
            ));
    }

    /**
     * 댓글 수정
     *
     * @param seq
     * @param commentDto
     * @return
     * @throws Exception
     */
    @PutMapping(value = "/comment/{seq}")
    public ResponseEntity<MessageDto> saveComment(
        @PathVariable("seq") Long seq,
        @RequestBody CommentsDto commentDto
    ) throws Exception {
        BoardComments comments = boardService.updateComment(seq, commentDto);
        return ResponseEntity.ok()
            .body(new MessageDto("success", ResponseDto.builder()
                .id(comments.getCommentSeq())
                .createdDate(comments.getCreateDate()).build()
            ));
    }

    /**
     * 댓글 삭제
     *
     * @param seq
     * @return
     * @throws Exception
     */
    @DeleteMapping(value = "/comment/{seq}")
    public ResponseEntity<MessageDto> deleteComment(
        @PathVariable("seq") Long seq
    ) throws Exception {
        boardService.deleteComment(seq);
        return ResponseEntity.ok().body(new MessageDto("success", seq));
    }

}

 

2-4. Service 추가

@Service
@RequiredArgsConstructor
public class BoardService {

    private final BoardRepository boardRepository;
    private final BoardLikesRepository boardLikesRepository;
    private final BoardCommentsRepository boardCommentsRepository;
    private final BoardMapper boardMapper;

    public Map<String, Object> getBoardList(
        int page,
        String search,
        String text
    ) {
        Map<String, Object> resultMap = new HashMap<>();

        long totalCount = 0;
        int offset = (page - 1) * 10;
        List<BoardInfo> boardList = boardRepository.getBoardList(offset, search, text);

        switch (search) {
            case "title":
                totalCount = boardRepository.countByBoardTitleLike("%" + text + "%");
                break;
            case "content":
                totalCount = boardRepository.countByBoardContentLike("%" + text + "%");
                break;
            default:
                totalCount = boardRepository.count();
                break;
        }

        resultMap.put("boardList", boardList);
        resultMap.put("boardCount", totalCount);

        return resultMap;
    }

    public BoardInfo getBoardInfo(Long seq) {
        Long userNo = SecurityUtil.getUserNo();

        return boardRepository.getBoardInfo(seq, userNo);
    }

    public Board saveBoard(BoardDto boardDto) {
        boardDto.setUserNo(SecurityUtil.getUserNo());
        Board board = boardMapper.toBoardEntity(boardDto);
        boardRepository.save(board);

        return board;
    }

    public Board updateBoard(
        Long seq,
        BoardDto boardDto
    ) throws Exception {
        Board existBoard = boardRepository.findById(seq)
            .orElseThrow(() -> new Exception("게시글이 존재하지 않습니다."));
        Long userNo = SecurityUtil.getUserNo();

        if (!Objects.equals(userNo, existBoard.getUserNo())) {
            throw new Exception("수정 권한이 없습니다.");
        }

        Board updateBoard = existBoard.toBuilder()
            .boardTitle(boardDto.getBoardTitle())
            .boardContent(boardDto.getBoardContent())
            .marketCode(boardDto.getMarketCode())
            .companyCode(boardDto.getCompanyCode()).build();

        boardRepository.save(updateBoard);

        return updateBoard;
    }

    @Transactional
    public void deleteBoard(Long seq) throws Exception {
        Board existBoard = boardRepository.findById(seq)
            .orElseThrow(() -> new Exception("게시글이 존재하지 않습니다."));
        Long userNo = SecurityUtil.getUserNo();

        if (!Objects.equals(userNo, existBoard.getUserNo())) {
            throw new Exception("삭제 권한이 없습니다.");
        }

        // 게시글 삭제
        boardRepository.delete(existBoard);

        // 게시글 좋아요 삭제
        boardLikesRepository.deleteByBoardSeq(seq);

        // 게시글 댓글 삭제
        boardCommentsRepository.deleteByBoardSeq(seq);
    }

    public void saveLikes(LikesDto likesDto) {
        Long userNo = SecurityUtil.getUserNo();
        BoardLikes existLike = boardLikesRepository.findByBoardSeqAndUserNo(likesDto.getBoardSeq(),
            userNo);

        if (existLike == null) {
            likesDto.setUserNo(userNo);
            BoardLikes likes = boardMapper.toLikesEntity(likesDto);
            boardLikesRepository.save(likes);
        } else {
            boardLikesRepository.delete(existLike);
        }
    }

    public List<CommentsDto> getCommentsList(Long boardSeq) {
        List<CommentsInfo> commentList = boardCommentsRepository.getCommentsList(boardSeq);
        List<CommentsDto> buildList = commentList.stream()
            .map(comment -> CommentsDto.builder()
                .commentSeq(comment.getCommentSeq())
                .boardSeq(comment.getBoardSeq())
                .parentSeq(comment.getParentSeq())
                .userId(comment.getUserId())
                .nickName(comment.getNickName())
                .marketName(comment.getMarketName())
                .companyName(comment.getCompanyName())
                .profileImageUrl(comment.getProfileImageUrl())
                .commentContent(comment.getCommentContent())
                .createDate(comment.getCreateDate()).build()
            )
            .collect(Collectors.toList());

        return buildCommentsList(buildList, 0);
    }

    private List<CommentsDto> buildCommentsList(
        List<CommentsDto> templateList,
        long parentSeq
    ) {
        return templateList.stream()
            .filter(node -> node.getParentSeq() == parentSeq)
            .peek(node -> node.setSubCommentsList(
                buildCommentsList(templateList, node.getCommentSeq())))
            .collect(Collectors.toList());
    }

    @Transactional
    public BoardComments saveComment(CommentsDto commentsDto) {
        // 댓글 저장
        commentsDto.setUserNo(SecurityUtil.getUserNo());
        BoardComments comments = boardMapper.toCommentsEntity(commentsDto);
        boardCommentsRepository.save(comments);

        return comments;
    }

    public BoardComments updateComment(
        long seq,
        CommentsDto commentDto
    ) throws Exception {
        BoardComments existComment = boardCommentsRepository.findById(seq)
            .orElseThrow(() -> new Exception("답변이 존재하지 않습니다."));
        BoardComments updateComment = existComment.toBuilder()
            .commentContent(commentDto.getCommentContent()).build();

        boardCommentsRepository.save(updateComment);

        return updateComment;
    }

    @Transactional
    public void deleteComment(Long seq) throws Exception {
        BoardComments existComment = boardCommentsRepository.findById(seq)
            .orElseThrow(() -> new Exception("답변이 존재하지 않습니다."));
        Long userNo = SecurityUtil.getUserNo();

        if (!Objects.equals(userNo, existComment.getUserNo())) {
            throw new Exception("삭제 권한이 없습니다.");
        }

        // 댓글 삭제
        boardCommentsRepository.delete(existComment);

        // 자식 댓글 삭제
        boardCommentsRepository.deleteByParentSeq(seq);
    }

}

 

서비스의 중요한 메소드만 체크해보겠습니다.

  • getBoardList(int page, String search, String text) : 게시글 조회시 리스트(10개씩)와 함께 게시글 총 개수를 리턴합니다. 검색 사용시에 search/text를 통해 제목/내용 검색을 처리합니다.
  • saveLikes(LikesDto likesDto) : 해당 사용자가 게시글에 좋아요를 저장한 이력이 있는지 체크합니다. 이력이 없다면 저장을, 있다면 삭제합니다. 
  • getCommentsList(Long boardSeq) : 게시글에 댓글 리스트를 조회합니다. buildCommentsList를 통해 대댓글이 있는 경우 subCommentsList에 값이 담길 수 있도록 처리했습니다.

테스트를 통해 추가된 API들을 확인해보겠습니다.

 

3. 테스트

[GET] /board/list?page={page} : 게시판 리스트 조회

curl --location 'http://localhost:9090/board/list?page=1' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0MTIzIiwiaWF0IjoxNzE4NzAwOTg0LCJleHAiOjE3MTg3MDI3ODR9.GKfz1MGlQE0fVi2Km58pchw3-fSOdcH-u25b2Ec8RILvLvvylaZHSVqnDT30770DgMexQAcu5wHbAx0JKmrL-Q' \
--header 'Cookie: refresh-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIiLCJpYXQiOjE3MTg3NzM4MDEsImV4cCI6MTcyMTM2NTgwMX0.5cxXO4y2oU2XdHd4dI76kbdI3wY7Att4OpH2iDVq7PZr4f8y1Qoxo2C6_OqCSxk-o1JStVktMo-ROvh_DCxZbw'

 

[POST] /board/save :  게시글 저장

curl --location 'http://localhost:9090/board/save' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI0IiwiaWF0IjoxNzE5NTUxNTIzLCJleHAiOjE3MTk1NTMzMjN9.bpUvKP8jrjMs5gzn9IaM86FG1qgr8ZX6HN5F9WuSwN8IDwd3N_KAuNR0UKYSTSFV7sHL-xebO0--qCrMImmZ8A' \
--header 'Content-Type: application/json' \
--header 'Cookie: refresh-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIiLCJpYXQiOjE3MTg3NzM4MDEsImV4cCI6MTcyMTM2NTgwMX0.5cxXO4y2oU2XdHd4dI76kbdI3wY7Att4OpH2iDVq7PZr4f8y1Qoxo2C6_OqCSxk-o1JStVktMo-ROvh_DCxZbw' \
--data '{
    "boardTitle": "제목 테스트",
    "boardContent": "안녕하세요",
    "marketCode": "",
    "companyCode": "",
    "userNo": 14
}'

 

[PUT] /board/save/{seq} :  게시글 수정

curl --location --request PUT 'http://localhost:9090/board/save/44' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI0IiwiaWF0IjoxNzE5NTUxNTIzLCJleHAiOjE3MTk1NTMzMjN9.bpUvKP8jrjMs5gzn9IaM86FG1qgr8ZX6HN5F9WuSwN8IDwd3N_KAuNR0UKYSTSFV7sHL-xebO0--qCrMImmZ8A' \
--header 'Content-Type: application/json' \
--header 'Cookie: refresh-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIiLCJpYXQiOjE3MTg3NzM4MDEsImV4cCI6MTcyMTM2NTgwMX0.5cxXO4y2oU2XdHd4dI76kbdI3wY7Att4OpH2iDVq7PZr4f8y1Qoxo2C6_OqCSxk-o1JStVktMo-ROvh_DCxZbw' \
--data '{
    "boardTitle": "제목 테스트 수정해봐요",
    "boardContent": "안녕히가세요",
    "marketCode": "",
    "companyCode": "",
    "userNo": 14
}'

 

[GET] /board/info?seq={seq} : 게시글 내용 조회

curl --location 'http://localhost:9090/board/info?seq=44' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI0IiwiaWF0IjoxNzE5NTUxNTIzLCJleHAiOjE3MTk1NTMzMjN9.bpUvKP8jrjMs5gzn9IaM86FG1qgr8ZX6HN5F9WuSwN8IDwd3N_KAuNR0UKYSTSFV7sHL-xebO0--qCrMImmZ8A' \
--header 'Cookie: refresh-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIiLCJpYXQiOjE3MTg3NzM4MDEsImV4cCI6MTcyMTM2NTgwMX0.5cxXO4y2oU2XdHd4dI76kbdI3wY7Att4OpH2iDVq7PZr4f8y1Qoxo2C6_OqCSxk-o1JStVktMo-ROvh_DCxZbw'

 

[DELETE] /board/item/{seq} :  게시글 삭제

curl --location --request DELETE 'http://localhost:9090/board/item/44' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI0IiwiaWF0IjoxNzE5NTUxNTIzLCJleHAiOjE3MTk1NTMzMjN9.bpUvKP8jrjMs5gzn9IaM86FG1qgr8ZX6HN5F9WuSwN8IDwd3N_KAuNR0UKYSTSFV7sHL-xebO0--qCrMImmZ8A' \
--header 'Content-Type: application/json' \
--header 'Cookie: refresh-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIiLCJpYXQiOjE3MTg3NzM4MDEsImV4cCI6MTcyMTM2NTgwMX0.5cxXO4y2oU2XdHd4dI76kbdI3wY7Att4OpH2iDVq7PZr4f8y1Qoxo2C6_OqCSxk-o1JStVktMo-ROvh_DCxZbw' \
--data '{   
}'

 

[POST] /board/like :  좋아요 등록

curl --location 'http://localhost:9090/board/like' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI0IiwiaWF0IjoxNzE5NTUxNTIzLCJleHAiOjE3MTk1NTMzMjN9.bpUvKP8jrjMs5gzn9IaM86FG1qgr8ZX6HN5F9WuSwN8IDwd3N_KAuNR0UKYSTSFV7sHL-xebO0--qCrMImmZ8A' \
--header 'Content-Type: application/json' \
--header 'Cookie: refresh-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIiLCJpYXQiOjE3MTg3NzM4MDEsImV4cCI6MTcyMTM2NTgwMX0.5cxXO4y2oU2XdHd4dI76kbdI3wY7Att4OpH2iDVq7PZr4f8y1Qoxo2C6_OqCSxk-o1JStVktMo-ROvh_DCxZbw' \
--data '{
    "boardSeq": "33",
    "userId": "test123"
}'

 

[GET] /board/comment/list?seq={seq} :  게시판 댓글 리스트 조회

curl --location 'http://localhost:9090/board/comment/list?seq=21' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI0IiwiaWF0IjoxNzE5NTUxNTIzLCJleHAiOjE3MTk1NTMzMjN9.bpUvKP8jrjMs5gzn9IaM86FG1qgr8ZX6HN5F9WuSwN8IDwd3N_KAuNR0UKYSTSFV7sHL-xebO0--qCrMImmZ8A' \
--header 'Cookie: refresh-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIiLCJpYXQiOjE3MTg3NzM4MDEsImV4cCI6MTcyMTM2NTgwMX0.5cxXO4y2oU2XdHd4dI76kbdI3wY7Att4OpH2iDVq7PZr4f8y1Qoxo2C6_OqCSxk-o1JStVktMo-ROvh_DCxZbw'

 

[POST] /board/comment :  댓글 저장

curl --location 'http://localhost:9090/board/comment' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI0IiwiaWF0IjoxNzE5NTUxNTIzLCJleHAiOjE3MTk1NTMzMjN9.bpUvKP8jrjMs5gzn9IaM86FG1qgr8ZX6HN5F9WuSwN8IDwd3N_KAuNR0UKYSTSFV7sHL-xebO0--qCrMImmZ8A' \
--header 'Content-Type: application/json' \
--header 'Cookie: refresh-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIiLCJpYXQiOjE3MTg3NzM4MDEsImV4cCI6MTcyMTM2NTgwMX0.5cxXO4y2oU2XdHd4dI76kbdI3wY7Att4OpH2iDVq7PZr4f8y1Qoxo2C6_OqCSxk-o1JStVktMo-ROvh_DCxZbw' \
--data '{
    "boardSeq": 21,
    "parentSeq": 0,
    "userNo": 14,
    "commentContent" : "안녕하세요. 반가워요."
}'

 

[PUT] /board/comment/{seq} :  댓글 수정

curl --location --request PUT 'http://localhost:9090/board/comment/62' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI0IiwiaWF0IjoxNzE5NTUxNTIzLCJleHAiOjE3MTk1NTMzMjN9.bpUvKP8jrjMs5gzn9IaM86FG1qgr8ZX6HN5F9WuSwN8IDwd3N_KAuNR0UKYSTSFV7sHL-xebO0--qCrMImmZ8A' \
--header 'Content-Type: application/json' \
--header 'Cookie: refresh-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIiLCJpYXQiOjE3MTg3NzM4MDEsImV4cCI6MTcyMTM2NTgwMX0.5cxXO4y2oU2XdHd4dI76kbdI3wY7Att4OpH2iDVq7PZr4f8y1Qoxo2C6_OqCSxk-o1JStVktMo-ROvh_DCxZbw' \
--data '{
    "boardSeq": 21,
    "parentSeq": 0,
    "userNo": 14,
    "commentContent" : "댓글 수정해볼까요"
}'

 

[DELETE] /board/comment/{seq} :  댓글 삭제

curl --location --request DELETE 'http://localhost:9090/board/comment/62' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI0IiwiaWF0IjoxNzE5NTUxNTIzLCJleHAiOjE3MTk1NTMzMjN9.bpUvKP8jrjMs5gzn9IaM86FG1qgr8ZX6HN5F9WuSwN8IDwd3N_KAuNR0UKYSTSFV7sHL-xebO0--qCrMImmZ8A' \
--header 'Content-Type: application/json' \
--header 'Cookie: refresh-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIiLCJpYXQiOjE3MTg3NzM4MDEsImV4cCI6MTcyMTM2NTgwMX0.5cxXO4y2oU2XdHd4dI76kbdI3wY7Att4OpH2iDVq7PZr4f8y1Qoxo2C6_OqCSxk-o1JStVktMo-ROvh_DCxZbw' \
--data '{
   
}'

 


이제 게시판 CRUD에 필요한 API 개발이 완료되었습니다. 다음 글에서 게시판 화면을 만들고 API 기능을 사용한 과정을 알아보겠습니다.

본 포스팅은 스터디를 통해 작성하게 되었습니다. 피드백은 언제나 환영입니다 :)

반응형
Contents

포스팅 주소를 복사했습니다.

이 글이 도움이 되었다면 공감 부탁드립니다.