[프로젝트4] 3. BoardDto 수정하기

rin·2020년 6월 21일
0
post-thumbnail

목표
1. BoardDto에 goodHistoryId 멤버 변수를 추가한다.
2. 1에 맞춰서 컨트롤러와 서비스를 수정한다.
3. 테스트 코드로 검증한다.

goodHistoryId 추가하기

이전 글에서 이야기 했듯이 delete api를 호출할 때 필요한 pk를 프론트로 전달하는 것을 빼먹었다. 🤦🏻
이번 게시글에선 그 부분을 수정하고 끝낼 것이다.

BoardDto

isLike 멤버변수 아래에 goodHistoryId를 추가하였다.

@NoArgsConstructor
@Getter
public class BoardDto{

    private long id;
    private UserDto writer;
    private String contents;
    private String title;
    private LocalDateTime createdAt;
    @Setter
    private long likePoint;
    @Setter
    private boolean isLike;
    @Setter
    private long goodHistoryId;

    public BoardDto(BoardEntity board) {
        this.writer = UserDto.of(board.getWriter());
        this.id = board.getId();
        this.contents = board.getContents();
        this.title = board.getTitle();
        this.createdAt = board.getCreatedAt();
    }

    public static BoardDto of(BoardEntity board) {
        return new BoardDto(board);
    }
}

CountGoodContentsHistoryVO

CountGoodContentsHistoryVO를 이용해서 데이터를 가져올 것이므로 long goodContentsHistoryId 멤버 변수를 추가한다. 이 변수에 pk인 row id값을 받아올 것이다.

@Getter
@Setter
public class CountGoodContentsHistoryVO {
    long goodContentsHistoryId;
    long groupId;
    long likeCount;
}

goodContentsHistory-mapper

countByBoardInAndUser 메소드의 쿼리문을 변경한다. 아래 이미지는 원래 코드이다.
boardId-userId 쌍은 항상 유일하므로 (동일한 글에 같은 사람이 두 번 이상 좋아요 할 수 없기 때문에 항상 데이터는 하나만 존재한다.) 굳이 Grouping 하고 count한 값을 가져올 필요가 없었다.

검색이 되지 않으면 아예 vo 리스트에 추가되지도 않을 것이고 검색된다면 likeCount는 항상 0일 것이다.

따라서 다음처럼 pk인 id를 가져오도록 변경하고 group by는 없애주었다.

    <select id="countByBoardInAndUser" resultType="goodHistoryVo">
        SELECT id as goodContentsHistoryId, boardId as groupId
        FROM good_contents_history
        WHERE boardId IN <foreach collection="boards" item="board" index='i' open="(" close=")" separator=",">#{board.id}</foreach>
            AND userId = #{user.id}
    </select>

GoodContentsHistoryMapperTest

새로 작성한 쿼리를 테스트 할 것이다.
이전에 작성했던 countByBoardInAndUser 메소드를 테스트하는 코드는 @Disabled 시켜주었다.

    @Test
    @DisplayName("특정 게시물 목록에 대해 해당 사용자가 좋아요 한 내역을 가져온다.")
    void countByBoardInAndUser_test(){
        List<BoardEntity> newBoards = getBoards(2);
        BoardEntity likeContents = newBoards.get(0);
        UserEntity userLoggedIn = getNonAuthorUser();
        GoodContentsHistoryEntity goodContentsHistoryEntity = GoodContentsHistoryEntity.builder().board(likeContents).user(userLoggedIn).build();
        saveBoardAndGoodHistory(newBoards, goodContentsHistoryEntity);

        List<CountGoodContentsHistoryVO> vos = sut.countByBoardInAndUser(newBoards, userLoggedIn);

        for (CountGoodContentsHistoryVO vo : vos){
            assertThat(vo.getGroupId(), equalTo(likeContents.getId()));
            assertThat(vo.getGoodContentsHistoryId(), equalTo(goodContentsHistoryEntity.getId()));
        }
    }

    private UserEntity getNonAuthorUser() {
        return userRepository.findAll().stream().filter(user -> user.equals(userEntity) == false).findFirst().get();
    }

    private void saveBoardAndGoodHistory(List<BoardEntity> newBoards, GoodContentsHistoryEntity goodContentsHistoryEntity) {
        boardRepository.saveAll(newBoards);
        goodContentsHistoryRepository.save(goodContentsHistoryEntity);
    }

assert문에서 좋아요한 내역의 boardId와 (pk)Id이 정확한지 비교한다.

BoardService

combineBoardDto 메소드(오버로드되어있음) 중 boardsLikeCountsByUser 메소드를 호출하는 메소드만 수정한다.

이전 코드이다.
boardId를 key로 검색하여 데이터가 "존재하면" true를 "존재하지 않으면" false를 isLike에 set하는 로직만 존재했다. 실제로 value에 해당하는 count(*) (어짜피 항상 1이다.)는 사용되지 않는 모습이다.

변경된 코드에는 데이터가 "존재하면" value에 해당하는 pk id를 goodHistoryId에 저장한다.

전체 코드는 아래와 같다.

    private List<BoardDto> combineBoardDto(List<BoardEntity> boardEntities, List<CountGoodContentsHistoryVO> boardsLikeCounts, List<CountGoodContentsHistoryVO> boardsLikeCountsByUser) {
        Map<Long, Long> boardsLikeCountsMap = boardsLikeCounts.stream().collect(Collectors.toMap(CountGoodContentsHistoryVO::getGroupId, CountGoodContentsHistoryVO::getLikeCount));
        Map<Long, Long> boardsLikeCountsByUserMap = boardsLikeCountsByUser.stream().collect(Collectors.toMap(CountGoodContentsHistoryVO::getGroupId, CountGoodContentsHistoryVO::getGoodContentsHistoryId));

        return boardEntities.stream().map(boardEntity -> {
            BoardDto boardDto = BoardDto.of(boardEntity);
            if (Optional.ofNullable(boardsLikeCountsMap.get(boardDto.getId())).isPresent()){
                boardDto.setLikePoint(boardsLikeCountsMap.get(boardDto.getId()));
            } else {
                boardDto.setLikePoint(0);
            }
            boardDto.setLike(Optional.ofNullable(boardsLikeCountsByUserMap.get(boardDto.getId())).isPresent());
            if (Optional.ofNullable(boardsLikeCountsByUserMap.get(boardDto.getId())).isPresent()) {
                boardDto.setGoodHistoryId(boardsLikeCountsByUserMap.get(boardDto.getId()));
            }
            return boardDto;
        }).collect(Collectors.toList());
    }

BoardServiceIntegrationTest

코드가 길어서 주석으로 설명했으니 바로 코드를 보도록 하자.

@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/applicationContext.xml"})
@Transactional
public class BoardServiceIntegrationTest {

    @Autowired
    private BoardService sut;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BoardRepository boardRepository;

    @Autowired
    private GoodContentsHistoryRepository goodContentsHistoryRepository;

    @Test
    @DisplayName("BoardDto가 제대로 만들어지는지 확인한다.")
    void combineBoardDto_test() {
        // 새로 추가할 board의 writer로 사용할 entity이다.
        UserEntity writer = userRepository.findAll().get(0);
        // writer와 다른 userEntity를 로그인한 계정으로 사용한다.
        UserEntity userLoggedIn = userRepository.findAll().get(1);
        
	// 추가할 board 개수이다.
        int pageSize = (int) (Math.random() * 5) + 1;
        // 추가한 board 중 로그인한 유저가 좋아요를 누를 개수이다.
        int likeContentsCount = (int) (Math.random() * pageSize) + 1;

	// 추가된 board 중 좋아요를 받은 board의 id 리스트이다.
        List<Long> likeBoardIds = new ArrayList<>();
        // 새로 좋아요를 한 내역(goodContentsHistory)의 id 리스트이다.
        List<Long> likeBoardHistoryIds = new ArrayList<>();
        for (int index = 0; index < pageSize; ++index) {
            // 새로운 게시글을 insert한다.
            BoardEntity boardEntity
                    = boardRepository.save(BoardEntity.builder()
                                                .writer(writer)
                                                .title("combineBoardDto 테스트" + index)
                                                .contents("내용")
                                                .build());
                                                
	    // likeContentsCount 만큼 좋아요 내역을 추가한다.
            if (index < likeContentsCount) {
                likeBoardIds.add(boardEntity.getId());
                GoodContentsHistoryEntity goodContentsHistoryEntity
                        = goodContentsHistoryRepository.save(GoodContentsHistoryEntity.builder()
                                                                    .board(boardEntity)
                                                                    .user(userLoggedIn)
                                                                    .build());
                likeBoardHistoryIds.add(goodContentsHistoryEntity.getId());
            }
        }

	// 테스트의 목적인 get 메소드를 수행한다.
	// get 메소드의 반환 타입은 PageDto<List<BoardDto>>이므로 
	// getContents() 메소드를 이용해 List<BoardDto>만 가져온다.
        List<BoardDto> target = sut.get(
                PageRequest.of(0, pageSize, Sort.by(Sort.Direction.DESC, "createdAt")),
                Optional.of(UserForm.builder().accountId(userLoggedIn.getAccountId()).build())
        ).getContents();

	// 그 중 좋아요 내역이 있는 데이터만 골라낸다.
        List<BoardDto> likeTarget = target.stream().filter(dto -> dto.isLike()).collect(Collectors.toList());

	// 좋아요 한 내역이 있는 데이터 개수가 실제로 good_contents_history 테이블에
	// insert 데이터 개수와 일치하는지 확인한다.
        assertThat(likeTarget.size(), equalTo(likeContentsCount));
        for (BoardDto boardDto : likeTarget) {
            // 좋아요한 글이 맞는지 확인한다.
            assertThat(likeBoardIds, hasItem(boardDto.getId()));
            // 좋아요한 내역이 맞는지 확인한다.
            assertThat(likeBoardHistoryIds, hasItems(boardDto.getGoodHistoryId()));
        }
    }

}

Front 수정

어떤 게시글의 모달을 띄운 상태로 반복해서 좋아요 버튼을 누른다고 생각해보자.
그럼 post api → delete api → post api ... 가 반복될 것인데, 화면에 나타나는 데이터와 서버에 전달해줘야하는 데이터에 매번 변화가 있어야 할 것이다.

예를 들면 post api 요청을 수행한 뒤 바로 delete api 요청을 보낸다면 goodHistoryId가 pathParam으로 포함되어야한다. 즉 post api 요청에 성공하면 goodHistoryId 값을 클라이언트에서도 보관하고 있어야 한단 것이다.

이전 글에선 이부분을 생각하지 않았기 때문에, board.hbs를 다음처럼 변경해주었다.

board.hbs

        var addLike = (boardId) => {
            $.ajax({
                method: 'POST',
                url: 'api/boards/' + boardId + "/good"
            }).done(function (response) {
                if(typeof response.message != 'undefined'){
                    alert(response.message);
                }else{
                    updateNowContentsByAddLike(nowBoardList[nowBoardIndex], response);
                }
            })
        }

        var cancelLike = (boardId, goodHistoryId) => {
            $.ajax({
                method: 'DELETE',
                url: 'api/boards/' + boardId + "/good/" + goodHistoryId
            }).done(function (response) {
                if(typeof response.message != 'undefined'){
                    alert(response.message);
                }else{
                    updateNowContentsByCancelLike(nowBoardList[nowBoardIndex]);
                }
            })
        }

        var updateNowContentsByAddLike = (nowBoard, response) => {
            var nowPoint = parseInt(nowBoard.likePoint);
            $('#boardModal').find('#likePoint').text(nowPoint+1);
            nowBoard.likePoint = nowPoint+1;
            nowBoard.like = true;
            nowBoard.goodHistoryId = response.id;
        }

        var updateNowContentsByCancelLike = (nowBoard) => {
            var nowPoint = parseInt(nowBoard.likePoint);
            $('#boardModal').find('#likePoint').text(nowPoint-1);
            nowBoard.likePoint = nowPoint-1;
            nowBoard.like = false;
            nowBoard.goodHistoryId = 0;
        }

비교를 위해서 이전코드를 가져와보았다. 🤔 (아래 이미지)
보다시피 요청에 성공한 경우(done의 else 부분, 초록색 박스)에 모달의 보이는 부분만 고쳐주고 있다. 수정 후 코드에서는 클라이언트가 가지고 있는 멤버 변수도 함께 변경한다.

BoardApiController, BoardService 수정

변경 후 코드를 보면 응답값에서 id를 뽑아와서 goodHistoryId에 주입해주고 있는데 이를 위해서 api 요청의 response data를 goodContentsHistoryEntity로 변경해준다. (이전에는 void 였음.)

🔎 BoardApiController

@PostMapping("/{id}/good")
public ResponseEntity<GoodContentsHistoryEntity> addGoodPoint(@PathVariable long id){
    if (httpSession.getAttribute("USER") == null) {
            throw new FreeBoardException(UserExceptionType.LOGIN_INFORMATION_NOT_FOUND);
    }
    return ResponseEntity.ok(boardService.addGoodPoint((UserForm) httpSession.getAttribute("USER"), id));
}

🔎 BoardService

public GoodContentsHistoryEntity addGoodPoint(UserForm userForm, long boardId) {
    UserEntity user = Optional.of(userRepository.findByAccountId(userForm.getAccountId())).orElseThrow(() -> new FreeBoardException(UserExceptionType.NOT_FOUND_USER));
    BoardEntity target = Optional.of(boardRepository.findById(boardId).get()).orElseThrow(() -> new FreeBoardException(BoardExceptionType.NOT_FOUNT_CONTENTS));

    goodContentsHistoryRepository.findByUserAndBoard(user, target).ifPresent(none -> {
            throw new FreeBoardException(GoodContentsHistoryExceptionType.HISTORY_ALREADY_EXISTS);
    });

    return goodContentsHistoryRepository.save(
                GoodContentsHistoryEntity.builder()
                        .board(target)
                        .user(user)
                        .build()
    );
}

그럼 코드는 끝이다! 🙋🏻
톰캣을 띄워보자

현재 로그인한 유저가 좋아요를 한 글은 goodHistoryId를 가지고 있음을 확인 할 수 있다.

좋아요 개수 부분을 클릭해보자post 요청 후 응답값
GoodContentsHistoryEntity를 반환받는다.

전체 코드는 github에서 확인할 수 있습니다.

profile
🌱 😈💻 🌱

0개의 댓글