[프로젝트4] 2. 좋아요와 관련된 정보를 클라이언트로 전달하기, 클라이언트->서버 요청 추가하기

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

목표
1. 클라이언트로 전달해주는 데이터에 좋아요 개수를 포함시키도록 변경한다.
2. 테스트 코드로 검증한다.
3. 프론트에서 api를 호출하는 함수를 추가한다.

클라이언트로 전달하는 데이터 변경하기

전달할 데이터 구상

front는 어떻게 바꿀 것인지 생각해보자 🤔

  1. 우선 데이터를 출력할 때, 좋아요 개수가 보일 것이며
  2. 지금 로그인한 계정의 좋아요 여부를 UI로 확인 할 수 있고.
  3. 그에 따라 [좋아요] 버튼 클릭시 다른 api 요청을 보낼 것이다.

따라서 서버에서 클라이언트로 전달해줘야 할 데이터는 2가지가 추가된다.
하나는 해당 글의 좋아요 개수이고, 다른 하나는 현재 로그인한 사용자의 특정 게시글에 대한 좋아요 유무이다. 이 두 데이터를 BoardDto에 담아서 전달해주도록 하겠다.

BoardDto

🤦🏻 good이라는 단어를 계속 쓰고 있었는데 뭔가 어색한 느낌이더만 Like가 더 일반적이라서 그랬나보다.. 다른 코드를 전부 고치는건 일이라 섞어쓰도록 하겠음 👀💦

각 게시글마다 좋아요 개수와 로그인한 유저가 좋아요한 것인이 판단하기 위한 멤버 변수를 추가하였다.

MyBatis 추가

이전 myBatis 관련 게시글은 여기에서 확인할 수 있습니다.

why?

JPA 잘 쓰다가 갑자기 무슨 MyBatis냐고 할 수도 있겠지만, 필요한 것은 good_contents_history특정 게시글에 대한 컬럼의 개수이기 때문에 GroupBy의 사용이 불가피하기 때문에 추가하기로 결정하였다.

select boardId, count(*) as totalLike
from good_contents_history
where boardId in ( ?, ?, ... )
group by boardId;

select boardId, count(*) as myLike
from good_contents_history
where boardId in ( ?, ?, ... ) and userId = ?
group by boardId;

사실 QueryDsl을 사용해도 된다. (실제로 group by 구문이 필요한 곳에서 사용해 보았음)
QueryDsl이 가독성이 떨어지고 진입장벽이 높다는 이유로 잘 사용되지 않는다는 이야기를 들었기 때문에 MyBatis를 사용해보려고 한다.

jpa의 네이티브 쿼리나 jpql을 사용해볼까 하기도 했는데 in절과 group by절을 맞춰주는 작업이 더 복잡하더라..

build.gradle

mybatis 관련 종속성을 추가해주자

// https://mvnrepository.com/artifact/org.mybatis/mybatis
    compile group: 'org.mybatis', name: 'mybatis', version: '3.5.4'
    // https://mvnrepository.com/artifact/org.mybatis/mybatis-spring
    compile group: 'org.mybatis', name: 'mybatis-spring', version: '2.0.4'

applicationContext

myBatis 관련 설정을 추가한다.

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
     <property name="dataSource" ref="dataSource"/>
     <property name="configLocation" value="classpath:mybatis-config.xml"/>
     <property name="mapperLocations" value="classpath:/mappers/*.xml"/>
</bean>

config파일과 mapper파일을 resources 하위에 생성해준다.

게시글 리스트에 대한 좋아요 개수 가져오기

Mapper와 VO 추가

goodContentsHistory 패키지 하위에 vo/CountGoodContentsHistoryVO 클래스를 생성하였다. 이 클래스는 단순히 MyBatis로 얻어온 카운트 값을 담기 위한 것이므로 엔티티가 아닌 POJO 객체이다.

VO는 ValueObject의 약자로써 각 인스턴스를 구별할 고유값이 존재하지 않고 포함하고 있는 변수값이 모두 동일하면 같은 인스턴스라고 말할 수 있다.

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

goodContentsHistory 패키지 하위에 CountGoodContentsHistoryVO 객체 리스트를 반환하는 메소드를 작성할 GoodContentsHistoryMapper 인터페이스를 생성해주자.

@Mapper
public interface GoodContentsHistoryMapper {
    List<CountGoodContentsHistoryVO> countByBoardIn(@Param("boards") List<BoardEntity> boards);
}

🔎 applicationContext
추가한 매퍼를 applicationContext에 빈으로 등록한다.

<bean id="goodContentsHistoryMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
     <property name="mapperInterface" value="com.freeboard04.domain.goodContentsHistory.GoodContentsHistoryMapper"/>
     <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties>
        <property name="id" value="id"/>
        <property name="createdAt" value="createdAt"/>
        <property name="updatedAt" value="updatedAt"/>

        <property name="good_contents_history" value="good_contents_history"/>
        <property name="boardId" value="boardId"/>
        <property name="userId" value="userId"/>

    </properties>

    <typeAliases>
        <typeAlias alias="goodHistoryVo" type="com.freeboard04.domain.goodContentsHistory.vo.CountGoodContentsHistoryVO"/>
    </typeAliases>
</configuration>

현재는 하나의 테이블에만 접근하기 때문에 사용하는 값만 셋팅해주었다.

goodContentsHistory-mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.freeboard04.domain.goodContentsHistory.GoodContentsHistoryMapper">
    <select id="countByBoardIn" resultType="goodHistoryVo">
        SELECT boardId as groupId, count(*) as likeCount
        FROM good_contents_history
        WHERE boardId IN <foreach collection="boards" item="board" index='i' open="(" close=")" separator=",">#{board.id}</foreach>
        GROUP BY boardId
    </select>
</mapper>

인자로 받은 List<BoardEntity>foreach 구문을 통해 IN절에 필요한 id 리스트 문자열로 변환한다.
반환 타입은 CountGoodContentsHistoryVO이며 mybatis-config 파일에서 alias 설정을 해줬기 때문에 위와같이 나타내었다.

테스트

GoodContentsHistoryMapperTest 테스트 클래스를 새로 만들어서 myBatis로 작성한 쿼리를 수행해본다.
전체 코드는 아래와 같다.

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

    @Autowired
    GoodContentsHistoryMapper sut;

    @Autowired
    UserRepository userRepository;

    @Autowired
    BoardRepository boardRepository;

    @Autowired
    GoodContentsHistoryRepository goodContentsHistoryRepository;

    UserEntity userEntity;

    @BeforeEach
    private void init() {
        userEntity = userRepository.findAll().get(0);
    }


    @Test
    void countByBoardIdIn_test() {
        int goodCount = getGoodCount();

        List<BoardEntity> boards = getBoards(2);
        insertGoodContentsHistory(boards, goodCount);

        List<CountGoodContentsHistoryVO> counts = sut.countByBoardIn(boards);

        for (CountGoodContentsHistoryVO count : counts){
            assertThat(boards.stream().map(boardEntity -> boardEntity.getId()).collect(Collectors.toList()), hasItem(count.getGroupId()));
            assertThat((int) count.getLikeCount(), equalTo(goodCount));
        }
    }

    private int getGoodCount() {
        int max = (int) userRepository.count() - 1;
        return (int) (Math.random() * (max)) + 1;
    }

    private void insertGoodContentsHistory(List<BoardEntity> newBoards, int goodCount) {
        boardRepository.saveAll(newBoards);
        List<UserEntity> userEntities = userRepository.findAll().stream().filter(user -> user.equals(userEntity) == false).collect(Collectors.toList());
        for (int i = 0; i < goodCount; ++i) {
            for (BoardEntity newBoard : newBoards) {
                goodContentsHistoryRepository.save(GoodContentsHistoryEntity.builder().user(userEntities.get(i)).board(newBoard).build());
            }
        }
    }

    private List<BoardEntity> getBoards(int size){
        List<BoardEntity> boards = new ArrayList<>();
        for (int i=1 ;i<=size; ++i){
            boards.add(BoardEntity.builder().contents(LocalDateTime.now() + "-test"+i).title(LocalDateTime.now() + "-test"+i).writer(userEntity).build());
        }
        return boards;
    }

}

게시글 리스트에 대한 특정 유저의 좋아요 여부 가져오기

위에서 작성한 메소드와 다른 점은 특정 유저가 좋아요한 내역이 있는지 판단한단 것이다. 즉 WHERE 절에 로그인 유저의 정보를 포함해 질의할 것이다.

GoodContentsHistoryMapper

게시글 리스트와 유저 엔티티를 인자로 받는 메소드를 선언한다.

List<CountGoodContentsHistoryVO> countByBoardInAndUser(@Param("boards") List<BoardEntity> boards, @Param("user")UserEntity user);

goodContentsHistory-mapper.xml

countByBoardIn 쿼리와 유사하다. 추가된 부분은 WHERE 절의 AND userId = #{user.id} 뿐이다.

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

GoodContentsHistoryMapperTest

    @Test
    @DisplayName("특정 게시물 목록에 대해 해당 사용자가 좋아요 한 내역을 가져온다.")
    void countByBoardInAndUser_test(){
        List<BoardEntity> newBoards = getBoards(2);
        BoardEntity likeContents = newBoards.get(0);
        UserEntity userLoggedIn = userRepository.findAll().stream().filter(user -> user.equals(userEntity) == false).findFirst().get();
        saveBoardAndGoodHistory(newBoards, likeContents, userLoggedIn);

        List<CountGoodContentsHistoryVO> vos = sut.countByBoardInAndUser(newBoards, userLoggedIn);
        
        for (CountGoodContentsHistoryVO vo : vos){
            assertThat(vo.getGroupId(), equalTo(likeContents.getId()));
            assertThat(vo.getLikeCount(), equalTo(1L));
        }
    }

    private void saveBoardAndGoodHistory(List<BoardEntity> newBoards, BoardEntity likeContents, UserEntity userLoggedIn) {
        boardRepository.saveAll(newBoards);
        goodContentsHistoryRepository.save(GoodContentsHistoryEntity.builder().board(likeContents).user(userLoggedIn).build());
    }

데이터 조합해서 클라이언트로 전달하기

결국 db 접근은 세 번이 이루어지는데 각 질의에서 가져온 데이터를 하나의 인스턴스로 만들어서 클라이언트에 전달해주어야한다.

따라서 BoardApiController와 BoardService를 수정할 것이다.

BoardService

🔎 이전 코드에서는 서비스 레이어에서 질의만 수행하고 반환 데이터로의 변환은 컨트롤러에서 수행했다.

// BoardService
public Page<BoardEntity> get(Pageable pageable) {
    return boardRepository.findAll(PageUtil.convertToZeroBasePageWithSort(pageable));
}

// BoardApiController
@GetMapping
public ResponseEntity<PageDto<BoardDto>> get(@PageableDefault(page = 1, size = 10, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
    Page<BoardEntity> pageBoardList = boardService.get(pageable);
    List<BoardDto> boardDtoList = pageBoardList.stream().map(boardEntity -> BoardDto.of(boardEntity)).collect(Collectors.toList());
    return ResponseEntity.ok(PageDto.of(pageBoardList, boardDtoList));
}

🔎 변경 코드된 코드의 서비스 레이어를 보자.
우선 반환 타입이 클라이언트로 전달할 반환 타입 PageDto<BoardDto>과 일치한다. 즉, 이전에 컨트롤러에서 수행하면 변환 작업을 서비스 레이어에서 수행하겠단 것이다.

public PageDto<BoardDto> get(Pageable pageable, Optional<UserForm> userLoggedIn) {
    Page<BoardEntity> boardEntityPage = boardRepository.findAll(PageUtil.convertToZeroBasePageWithSort(pageable));
    List<BoardEntity> boardEntities = boardEntityPage.getContent();
    List<CountGoodContentsHistoryVO> boardsLikeCounts = goodContentsHistoryMapper.countByBoardIn(boardEntities);

    if (userLoggedIn.isPresent()) {
        UserEntity user = userRepository.findByAccountId(userLoggedIn.get().getAccountId());
        List<CountGoodContentsHistoryVO> boardsLikeCountsByUser = goodContentsHistoryMapper.countByBoardInAndUser(boardEntities, user);
        return PageDto.of(boardEntityPage, boardDtos);
    }

    List<BoardDto> boardDtos = combineBoardDto(boardEntities, boardsLikeCounts);
        return PageDto.of(boardEntityPage, boardDtos);
}

중간에 분기문이 하나 있는데 로그인한 경우와 로그인 하지 않은 경우로 나눈 것이다. 로그인한 경우에만 countByBoardInAndUser 메소드를 호출한다.

combineBoardDto메소드는 다음과 같이 오버로딩되어있다.

// 로그인 하지 않은 경우
private List<BoardDto> combineBoardDto(List<BoardEntity> boardEntities, List<CountGoodContentsHistoryVO> boardsLikeCounts) {
    Map<Long, Long> boardsLikeCountsMap = boardsLikeCounts.stream().collect(Collectors.toMap(CountGoodContentsHistoryVO::getGroupId, CountGoodContentsHistoryVO::getLikeCount));

    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(false); // 로그인한 계정이 없으므로 무조건 false 처리
        return boardDto;
    }).collect(Collectors.toList());
}

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::getLikeCount));

    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);
        }
        // Map에서 해당 글을 찾을 수 있다면 내역이 저장된 것 = 즉 해당 계정으로 좋아요 한 것 = true      
        boardDto.setLike(Optional.ofNullable(boardsLikeCountsByUserMap.get(boardDto.getId())).isPresent());
        return boardDto;
    }).collect(Collectors.toList());
}

Map을 사용한 이유는 List를 순차 순회하는 것보다 HashMap의 key로 탐색하는 것이 빠를 것이라고 생각해서이다. (사실 한 번에 가져오는 데이터가 많지 않아서 성능 차이는 거의 없을거 같긴하다 🤔)

BoardApiController

서비스 레이어에서 복잡한 내용을 다 수행하도록 하였기 때문에 api 메소드는 간단해졌다.

@GetMapping
public ResponseEntity<PageDto<BoardDto>> get(@PageableDefault(page = 1, size = 10, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
    return ResponseEntity.ok(boardService.get(pageable, Optional.ofNullable((UserForm) httpSession.getAttribute("USER"))));
}

BoardServiceUnitTest

Mock Test를 진행한다.

    @Test
    @DisplayName("로그인하지 않은 경우에 데이터를 요청하면 isLike는 false로 셋팅된다.")
    public void newGetTest1(){
        long groupId = 1L;
        long likePoint = 10L;
        int pageNumber = 1;
        int pageSize = 1;

        Page<BoardEntity> spyBoardEntityPage = getSpyBoardEntityPage(groupId, pageNumber, pageSize);
        List<CountGoodContentsHistoryVO> spyCountGoodHistory = getSpyCountGoodContentsHistoryVOS(groupId, likePoint);

        given(mockBoardRepo.findAll(PageUtil.convertToZeroBasePage(PageRequest.of(pageNumber, pageSize)))).willReturn(spyBoardEntityPage);
        given(mockGoodHistoryMapper.countByBoardIn(anyList())).willReturn(spyCountGoodHistory);

        PageDto<BoardDto> result = sut.get(PageRequest.of(pageNumber, pageSize), Optional.empty());

        assertThat(result.getContents().get(0).getLikePoint(), equalTo(likePoint));
        assertThat(result.getContents().get(0).isLike(), equalTo(false));
    }

    @Test
    @DisplayName("로그인 경우에 데이터를 요청하면 좋아요한 경우 isLike는 true로 셋팅된다.")
    public void newGetTest2(){
        long groupId = 1L;
        long likePoint = 10L;
        int pageNumber = 1;
        int pageSize = 1;

        Page<BoardEntity> spyBoardEntityPage = getSpyBoardEntityPage(groupId, pageNumber, pageSize);
        List<CountGoodContentsHistoryVO> spyCountGoodHistory = getSpyCountGoodContentsHistoryVOS(groupId, likePoint);

        given(mockBoardRepo.findAll(PageUtil.convertToZeroBasePage(PageRequest.of(pageNumber, pageSize)))).willReturn(spyBoardEntityPage);
        given(mockGoodHistoryMapper.countByBoardIn(anyList())).willReturn(spyCountGoodHistory);
        given(mockUserRepo.findByAccountId(anyString())).willReturn(new UserEntity());
        given(mockGoodHistoryMapper.countByBoardInAndUser(anyList(), any())).willReturn(spyCountGoodHistory);

        PageDto<BoardDto> result = sut.get(PageRequest.of(pageNumber, pageSize), Optional.of(UserForm.builder().accountId("mockTest").build()));

        assertThat(result.getContents().get(0).getLikePoint(), equalTo(likePoint));
        assertThat(result.getContents().get(0).isLike(), equalTo(true));
    }

    private Page<BoardEntity> getSpyBoardEntityPage(long groupId, int pageNumber, int pageSize) {
        return spy(new Page<BoardEntity>() {
                @Override
                public int getTotalPages() {
                    return 0;
                }

                @Override
                public long getTotalElements() {
                    return 0;
                }

                @Override
                public <U> Page<U> map(Function<? super BoardEntity, ? extends U> converter) {
                    return null;
                }

                @Override
                public int getNumber() {
                    return pageNumber;
                }

                @Override
                public int getSize() {
                    return pageSize;
                }

                @Override
                public int getNumberOfElements() {
                    return 0;
                }

                @Override
                public List<BoardEntity> getContent() {
                    List<BoardEntity> boardEntities = spy(new ArrayList<>());
                    BoardEntity boardEntity = BoardEntity.builder().writer(new UserEntity()).build();
                    boardEntity.setId(groupId);
                    boardEntities.add(boardEntity);

                    return boardEntities;
                }

                @Override
                public boolean hasContent() {
                    return false;
                }

                @Override
                public Sort getSort() {
                    return Sort.unsorted();
                }

                @Override
                public boolean isFirst() {
                    return false;
                }

                @Override
                public boolean isLast() {
                    return false;
                }

                @Override
                public boolean hasNext() {
                    return false;
                }

                @Override
                public boolean hasPrevious() {
                    return false;
                }

                @Override
                public Pageable nextPageable() {
                    return null;
                }

                @Override
                public Pageable previousPageable() {
                    return null;
                }

                @Override
                public Iterator<BoardEntity> iterator() {
                    return null;
                }
            });
    }

    private List<CountGoodContentsHistoryVO> getSpyCountGoodContentsHistoryVOS(long groupId, long likeCount) {
        List<CountGoodContentsHistoryVO> countGoodContentsHistoryVOS = spy(new ArrayList<>());
        CountGoodContentsHistoryVO vo = new CountGoodContentsHistoryVO();
        vo.setGroupId(groupId);
        vo.setLikeCount(likeCount);
        countGoodContentsHistoryVOS.add(vo);

        return countGoodContentsHistoryVOS;
    }

검색어를 입력한 경우

검색어를 입력한 경우에는 다른 api요청과 서비스 메소드를 사용하므로 이 부분도 변경해 주도록하자.

BoardService

board 테이블에 질의하는 부분은 getBoardEntityPageByKeyword 메소드로 분리하였고 search 메소드는 필요한 메소드를 호출하는 로직으로 구성하였다.

public PageDto<BoardDto> search(Pageable pageable, String keyword, SearchType type, UserForm userForm) {
    Page<BoardEntity> boardEntityPage = getBoardEntityPageByKeyword(pageable, keyword, type);
        
    List<BoardEntity> boardEntities = boardEntityPage.getContent();
    List<CountGoodContentsHistoryVO> boardsLikeCounts = goodContentsHistoryMapper.countByBoardIn(boardEntities);

    UserEntity userLoggedIn = userRepository.findByAccountId(userForm.getAccountId());
    List<CountGoodContentsHistoryVO> boardsLikeCountsByUser = goodContentsHistoryMapper.countByBoardInAndUser(boardEntities, userLoggedIn);
        
    List<BoardDto> boardDtos = combineBoardDto(boardEntities, boardsLikeCounts, boardsLikeCountsByUser);
    return PageDto.of(boardEntityPage, boardDtos);
}

private Page<BoardEntity> getBoardEntityPageByKeyword(Pageable pageable, String keyword, SearchType type) {
    if (type.equals(SearchType.WRITER)) {
         List<UserEntity> userEntityList = userRepository.findAllByAccountIdLike("%" + keyword + "%");
         return boardRepository.findAllByWriterIn(userEntityList, PageUtil.convertToZeroBasePageWithSort(pageable));
    } else {
         Specification<BoardEntity> spec = Specification.where(BoardSpecs.hasContents(keyword, type))
                    .or(BoardSpecs.hasTitle(keyword, type));
         return boardRepository.findAll(spec, PageUtil.convertToZeroBasePageWithSort(pageable));
    }
}

BoardApiController

변경 전 코드

변경 후 코드

@GetMapping(params = {"type", "keyword"})
public ResponseEntity<PageDto<BoardDto>> search(@PageableDefault(page = 1, size = 10, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable,
                                                 @RequestParam String keyword, @RequestParam SearchType type) {
    if (httpSession.getAttribute("USER") == null) {
         throw new FreeBoardException(UserExceptionType.LOGIN_INFORMATION_NOT_FOUND);
    }
    return ResponseEntity.ok(boardService.search(pageable, keyword, type, (UserForm) httpSession.getAttribute("USER")));
}

BoardServiceUnitTest

    @Test
    @DisplayName("작성자로 검색한 경우 - 데이터가 정확하게 합쳐지는지 확인한다.")
    public void searchTest1() {
        long groupId = 1L;
        long likePoint = 10L;
        int pageNumber = 1;
        int pageSize = 1;

        Page<BoardEntity> spyBoardEntityPage = getSpyBoardEntityPage(groupId, pageNumber, pageSize);
        List<CountGoodContentsHistoryVO> spyCountGoodHistory = getSpyCountGoodContentsHistoryVOS(groupId, likePoint);

        given(mockUserRepo.findAllByAccountIdLike(anyString())).willReturn(Lists.emptyList());
        given(mockBoardRepo.findAllByWriterIn(Lists.emptyList(), PageUtil.convertToZeroBasePage(PageRequest.of(pageNumber, pageSize)))).willReturn(spyBoardEntityPage);
        given(mockGoodHistoryMapper.countByBoardIn(anyList())).willReturn(spyCountGoodHistory);
        given(mockUserRepo.findByAccountId(anyString())).willReturn(new UserEntity());
        given(mockGoodHistoryMapper.countByBoardInAndUser(anyList(), any())).willReturn(spyCountGoodHistory);

        PageDto<BoardDto> result = sut.search(PageRequest.of(pageNumber, pageSize), anyString(), SearchType.WRITER,  UserForm.builder().accountId("mockTest").build());

        assertThat(result.getContents().get(0).getLikePoint(), equalTo(likePoint));
        assertThat(result.getContents().get(0).isLike(), equalTo(true));
    }

front

테이블과 게시글 상세보기에 좋아요 개수를 노출하고 클릭 했을 때 이벤트를 수행 할 수 있도록 하자.

table.hbs

변경 된 부분View
<script id="tableList" type="text/x-handlebars-template">
    <table class="table table-striped" style="width: 50% !important;">
        <thead>
        <tr>
            <th scope="col">#</th>
            <th scope="col">제목</th>
            <th scope="col">글쓴이</th>
            <th scope="col">Like</th>
        </tr>
        </thead>
        <tbody>
        \{{#contents}}
        <tr onclick='fetchDetails(\{{@index}})'>
            <th scope="row">\{{math @index '+' 1}}</th>
            <td>\{{title}}</td>
            <td>\{{writer.accountId}}</td>
            <td>\{{likePoint}}</td>
        </tr>
       \{{/contents}}
        </tbody>
    </table>
</script>

modal.hbs

변경 된 부분View
<script id="writeModal" type="text/x-handlebars-template">
    <div class="modal fade" id="boardModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
        <div class="modal-dialog" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="exampleModalLabel">New message</h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">
                    <form>
                        <div class="form-group">
                            <label for="recipient-name" class="col-form-label">Writer:</label>
                            <input type="text" class="form-control" id="user" disabled="disabled" value="\{{this}}" required />
                        </div>
                        <div class="form-group">
                            <label for="message-text" class="col-form-label">Title:</label>
                            <textarea class="form-control" id="title" required></textarea>
                        </div>
                        <div class="form-group">
                            <label for="message-text" class="col-form-label">Contents:</label>
                            <textarea class="form-control" id="contents" required></textarea>
                        </div>
                        <div class="form-group">
                            Like: <span id="likePoint" onclick="handleLikeClick()"></span> ❤️
                        </div>
                        <input type="submit" id="requiredBtn" style="visibility: hidden;" />
                    </form>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal" id="closeBtn">Close</button>
                    <button type="button" class="btn btn-primary" id="saveBtn">Write</button>
                    <button type="button" class="btn btn-primary" id="deleteBtn" style="display: none">Delete</button>
                </div>
            </div>
        </div>
    </div>
</script>

board.hbs

추가된 코드는 아래와 같다.

        var handleLikeClick = () => {
            if (nowBoardList[nowBoardIndex].like === true){
                cancelLike(nowBoardList[nowBoardIndex].id, nowBoardList[nowBoardIndex].goodHistoryId)
            } else {
                addLike(nowBoardList[nowBoardIndex].id)
            }
        }

        var addLike = (boardId) => {
            $.ajax({
                method: 'POST',
                url: 'api/boards/' + boardId + "/good"
            }).done(function (response) {
                if(typeof response.message != 'undefined'){
                    alert(response.message);
                }else{
                    var nowPoint = parseInt(nowBoardList[nowBoardIndex].likePoint);
                    $('#boardModal').find('#likePoint').text(nowPoint+1);
                    nowBoardList[nowBoardIndex].like = true;
                }
            })
        }

        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{
                    var nowPoint = parseInt(nowBoardList[nowBoardIndex].likePoint);
                    $('#boardModal').find('#likePoint').text(nowPoint-1);
                    nowBoardList[nowBoardIndex].like = false;
                }
            })
        }

모달에서 좋아요 개수(숫자)를 클릭하면 handleLikeClick 함수가 호출되고 현재 로그인한 유저가 해당 글을 이미 좋아요했는지 판단한 다음 서버로 각기 다른 api 요청을 보낸다.

요청에 성공한 경우에는 view에 보이는 값은 +1 (좋아요) 혹은 -1 (좋아요 취소) 하는 것으로 로직이 끝난다.

🤦🏻 약간(?)의 문제를 찾아냈는데 서버에 좋아요 취소 요청을 보낼 때는 내역을 삭제하기 위해서 이전에 좋아요할 때 추가된 내역의 고유 아이디 goodHistoryId를 보내야하는데 BoardDto를 생성할 때 이부분을 빼먹어버렸다.
현재 서버에서 받아오는 정보는 위와 같다.
따라서 cancelLike 함수를 수행하려고하면 에러가 발생할 것인데.. 서버측 로직을 다시 바꿔줄 것이기에 프론트는 최종적으로 원하는 로직으로 작성해두었다.

다음 글에서 BoardDto에 goodHistoryId를 추가하고 레파지토리 관련 로직도 싹 바꾸도록 하겠다.

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

profile
🌱 😈💻 🌱

0개의 댓글