목표
1. 클라이언트로 전달해주는 데이터에 좋아요 개수를 포함시키도록 변경한다.
2. 테스트 코드로 검증한다.
3. 프론트에서 api를 호출하는 함수를 추가한다.
front는 어떻게 바꿀 것인지 생각해보자 🤔
따라서 서버에서 클라이언트로 전달해줘야 할 데이터는 2가지가 추가된다.
하나는 해당 글의 좋아요 개수이고, 다른 하나는 현재 로그인한 사용자의 특정 게시글에 대한 좋아요 유무이다. 이 두 데이터를 BoardDto에 담아서 전달해주도록 하겠다.
🤦🏻 good이라는 단어를 계속 쓰고 있었는데 뭔가 어색한 느낌이더만
Like
가 더 일반적이라서 그랬나보다.. 다른 코드를 전부 고치는건 일이라 섞어쓰도록 하겠음 👀💦
각 게시글마다 좋아요 개수와 로그인한 유저가 좋아요한 것인이 판단하기 위한 멤버 변수를 추가하였다.
이전 myBatis 관련 게시글은 여기에서 확인할 수 있습니다.
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절을 맞춰주는 작업이 더 복잡하더라..
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'
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
하위에 생성해준다.
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>
<?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>
현재는 하나의 테이블에만 접근하기 때문에 사용하는 값만 셋팅해주었다.
<?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
절에 로그인 유저의 정보를 포함해 질의할 것이다.
게시글 리스트와 유저 엔티티를 인자로 받는 메소드를 선언한다.
List<CountGoodContentsHistoryVO> countByBoardInAndUser(@Param("boards") List<BoardEntity> boards, @Param("user")UserEntity user);
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>
@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
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로 탐색하는 것이 빠를 것이라고 생각해서이다. (사실 한 번에 가져오는 데이터가 많지 않아서 성능 차이는 거의 없을거 같긴하다 🤔)
서비스 레이어에서 복잡한 내용을 다 수행하도록 하였기 때문에 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"))));
}
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요청과 서비스 메소드를 사용하므로 이 부분도 변경해 주도록하자.
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));
}
}
변경 전 코드
변경 후 코드
@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")));
}
@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));
}
테이블과 게시글 상세보기에 좋아요 개수를 노출하고 클릭 했을 때 이벤트를 수행 할 수 있도록 하자.
변경 된 부분 | 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>
변경 된 부분 | 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">×</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>
추가된 코드는 아래와 같다.
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에서 확인 할 수 있습니다.