Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[✨feat] 특정 옵션 좋아요 삭제 #87

Merged
merged 8 commits into from
Feb 26, 2025
20 changes: 16 additions & 4 deletions src/main/java/org/noostak/likes/api/LikeController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.noostak.global.success.SuccessResponse;
import org.noostak.likes.application.LikeService;
import org.noostak.likes.common.success.LikesSuccessCode;
import org.noostak.likes.dto.DecreaseResponse;
import org.noostak.likes.dto.IncreaseResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
Expand All @@ -17,13 +18,24 @@ public class LikeController {

@PostMapping("/{appointmentId}/appointment-options/{appointmentOptionId}/like")
public ResponseEntity<?> increase(
@RequestAttribute("memberId") Long memberId,
@PathVariable long appointmentId,
@PathVariable long appointmentOptionId){
@RequestAttribute("memberId") Long memberId,
@PathVariable long appointmentId,
@PathVariable long appointmentOptionId) {

IncreaseResponse response = likeService.increase(memberId, appointmentId, appointmentOptionId);

return ResponseEntity.ok(SuccessResponse.of(LikesSuccessCode.LIKE_CREATED,response));
return ResponseEntity.ok(SuccessResponse.of(LikesSuccessCode.LIKE_CREATED, response));
}

@DeleteMapping("/{appointmentId}/appointment-options/{appointmentOptionId}/like")
public ResponseEntity<?> decrease(
@RequestAttribute("memberId") Long memberId,
@PathVariable long appointmentId,
@PathVariable long appointmentOptionId) {

DecreaseResponse response = likeService.decrease(memberId, appointmentId, appointmentOptionId);

return ResponseEntity.ok(SuccessResponse.of(LikesSuccessCode.LIKE_DELETED, response));
}

}
4 changes: 4 additions & 0 deletions src/main/java/org/noostak/likes/application/LikeService.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package org.noostak.likes.application;

import org.noostak.likes.dto.DecreaseResponse;
import org.noostak.likes.dto.IncreaseResponse;

public interface LikeService {
IncreaseResponse increase(Long memberId, Long appointmentId, Long appointmentOptionId);

DecreaseResponse decrease(Long memberId, Long appointmentId, Long appointmentOptionId);

Long getLikeCountByOptionId(Long optionId);
}
30 changes: 29 additions & 1 deletion src/main/java/org/noostak/likes/application/LikeServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.noostak.likes.common.exception.LikesException;
import org.noostak.likes.domain.Like;
import org.noostak.likes.domain.LikeRepository;
import org.noostak.likes.dto.DecreaseResponse;
import org.noostak.likes.dto.IncreaseResponse;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -32,7 +33,16 @@ public IncreaseResponse increase(Long memberId, Long appointmentId, Long appoint
createLike(memberId, appointmentId, appointmentOptionId);

long likes = getLikeCountByOptionId(appointmentOptionId);
return IncreaseResponse.of(likes+1);
return IncreaseResponse.of(likes);
}

@Override
@Transactional
public DecreaseResponse decrease(Long memberId, Long appointmentId, Long appointmentOptionId) {
deleteLike(memberId, appointmentId, appointmentOptionId);

long likes = getLikeCountByOptionId(appointmentOptionId);
return DecreaseResponse.of(likes);
}

@Override
Expand All @@ -41,6 +51,24 @@ public Long getLikeCountByOptionId(Long appointmentOptionId) {
}


private void deleteLike(Long memberId, Long appointmentId, Long appointmentOptionId) {

long count = getLikeCountByOptionId(appointmentOptionId);

if(count == 0){
throw new LikesException(LikesErrorCode.LIKES_NOT_NEGATIVE);
}

AppointmentMember appointmentMember =
appointmentMemberRepository
.findByMemberIdAndAppointmentId(memberId, appointmentId)
.orElseThrow(()->new AppointmentMemberException(AppointmentErrorCode.APPOINTMENT_NOT_FOUND));

Long appointmentMemberId = appointmentMember.getId();

likeRepository.deleteLikeByAppointmentMemberIdAndOptionId(appointmentMemberId,appointmentOptionId);
}

private void createLike(Long memberId, Long appointmentId, Long appointmentOptionId) {

long count = getLikeCountByOptionId(appointmentOptionId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
@AllArgsConstructor
public enum LikesSuccessCode implements SuccessCode {
LIKE_CREATED(HttpStatus.CREATED, "좋아요가 생성되었습니다."),
LIKE_DELETED(HttpStatus.OK, "좋아요가 삭제되었습니다."),

;

Expand Down
8 changes: 8 additions & 0 deletions src/main/java/org/noostak/likes/domain/LikeRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ public interface LikeRepository extends JpaRepository<Like, Long> {
@Query(nativeQuery = true, value = "SELECT COUNT(*) FROM likes WHERE appointment_member_id = :appointmentMemberId")
int getLikeCountByAppointmentMemberId(@Param("appointmentMemberId") Long appointmentMemberId);

@Query(nativeQuery = true,
value = "DELETE " +
"FROM likes " +
"WHERE appointment_member_id = :appointmentMemberId AND appointment_option_id = :optionId")
void deleteLikeByAppointmentMemberIdAndOptionId(
@Param("appointmentMemberId") Long appointmentMemberId,
@Param("optionId") Long optionId
);

@Query(nativeQuery = true,
value = "SELECT 1 " +
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/org/noostak/likes/dto/DecreaseResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.noostak.likes.dto;


import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class DecreaseResponse {
long likes;

public static DecreaseResponse of(long likes){
return new DecreaseResponse(likes);
}
}
88 changes: 86 additions & 2 deletions src/test/java/org/noostak/likes/domain/LikeServiceImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.noostak.appointmentoption.domain.AppointmentOption;
import org.noostak.likes.common.exception.LikesErrorCode;
import org.noostak.likes.common.exception.LikesException;
import org.noostak.likes.dto.DecreaseResponse;
import org.noostak.likes.dto.IncreaseResponse;

import java.util.Optional;
Expand Down Expand Up @@ -44,9 +45,31 @@ class LikeServiceImplTest {
private FakeAppointmentMemberRepository appointmentMemberRepository;

@Nested
@DisplayName("좋아요 증가 (Success)")
@DisplayName("좋아요 감소 (Success)")
class Success {

@Test
@DisplayName("좋아요를 삭제하면 개수가 감소한다.")
void decreaseLike() {
// given
Long memberId = 1L;
Long appointmentId = 2L;
Long appointmentOptionId = 3L;
int initialLikes = 5;

Mockito.lenient().when(optionRepository.getById(appointmentOptionId)).thenReturn(mockOption);
Mockito.lenient().when(appointmentMemberRepository.findByMemberIdAndAppointmentId(memberId, appointmentId))
.thenReturn(Optional.of(mockMember));
Mockito.lenient().when(likeRepository.getLikeCountByOptionId(appointmentOptionId)).thenReturn(initialLikes);

// when
DecreaseResponse response = likeService.decrease(memberId, appointmentId, appointmentOptionId);

// then
verify(likeRepository, times(1)).deleteLikeByAppointmentMemberIdAndOptionId(anyLong(), anyLong());
assertThat(response.getLikes()).isEqualTo(initialLikes);
}

@Test
@DisplayName("좋아요를 추가하면 갯수가 증가한다.")
void increaseLike() {
Expand All @@ -72,7 +95,43 @@ void increaseLike() {
@Nested
@DisplayName("좋아요 증가 (Failure)")
class Failure {
@Test
@DisplayName("좋아요 개수가 0이면 예외 발생")
void decreaseLike_Fail_LikesNotNegative() {
// given
Long memberId = 1L;
Long appointmentId = 2L;
Long appointmentOptionId = 3L;

when(likeRepository.getLikeCountByOptionId(appointmentOptionId)).thenReturn(0);

// when & then
assertThatThrownBy(() -> likeService.decrease(memberId, appointmentId, appointmentOptionId))
.isInstanceOf(LikesException.class)
.hasMessageContaining(LikesErrorCode.LIKES_NOT_NEGATIVE.getMessage());

verify(likeRepository, never()).deleteLikeByAppointmentMemberIdAndOptionId(anyLong(), anyLong());
}

@Test
@DisplayName("회원이 약속에 참여하지 않은 경우 예외 발생")
void decreaseLike_Fail_AppointmentMemberNotFound() {
// given
Long memberId = 1L;
Long appointmentId = 2L;
Long appointmentOptionId = 3L;

when(likeRepository.getLikeCountByOptionId(appointmentOptionId)).thenReturn(5);
when(appointmentMemberRepository.findByMemberIdAndAppointmentId(memberId, appointmentId))
.thenReturn(Optional.empty());

// when & then
assertThatThrownBy(() -> likeService.decrease(memberId, appointmentId, appointmentOptionId))
.isInstanceOf(AppointmentMemberException.class)
.hasMessage(AppointmentErrorCode.APPOINTMENT_NOT_FOUND.getMessage());

verify(likeRepository, never()).deleteLikeByAppointmentMemberIdAndOptionId(anyLong(), anyLong());
}
@Test
@DisplayName("좋아요 개수가 최대값을 초과하면 예외 발생")
void increaseLike_Fail_OverMaxLikes() {
Expand Down Expand Up @@ -118,7 +177,6 @@ void increaseLike_Fail_AppointmentMemberNotFound() {
}

// ======================== Fake Service ========================

static class FakeLikeService {
private final FakeAppointmentOptionRepository optionRepository;
private final FakeAppointmentMemberRepository appointmentMemberRepository;
Expand All @@ -137,6 +195,12 @@ public IncreaseResponse increase(Long memberId, Long appointmentId, Long appoint
return IncreaseResponse.of(likes);
}

public DecreaseResponse decrease(Long memberId, Long appointmentId, Long appointmentOptionId) {
deleteLike(memberId, appointmentId, appointmentOptionId);
int likes = getLikeCountByOptionId(appointmentOptionId);
return DecreaseResponse.of(likes);
}

public int getLikeCountByOptionId(Long appointmentOptionId) {
return likeRepository.getLikeCountByOptionId(appointmentOptionId);
}
Expand All @@ -154,15 +218,35 @@ private void createLike(Long memberId, Long appointmentId, Long appointmentOptio
Like newLike = Like.of(appointmentMember, appointmentOption);
likeRepository.save(newLike);
}

private void deleteLike(Long memberId, Long appointmentId, Long appointmentOptionId) {
int count = getLikeCountByOptionId(appointmentOptionId);

if (count == 0) {
throw new LikesException(LikesErrorCode.LIKES_NOT_NEGATIVE);
}

AppointmentMember appointmentMember =
appointmentMemberRepository.findByMemberIdAndAppointmentId(memberId, appointmentId)
.orElseThrow(() -> new AppointmentMemberException(AppointmentErrorCode.APPOINTMENT_NOT_FOUND));

Long appointmentMemberId = appointmentMember.getId();
likeRepository.deleteLikeByAppointmentMemberIdAndOptionId(appointmentMemberId, appointmentOptionId);
}
}


// ======================== Fake Repository ========================

static class FakeLikeRepository {
void save(Like like) {}

int getLikeCountByOptionId(Long appointmentOptionId) { return 0; }

void deleteLikeByAppointmentMemberIdAndOptionId(Long appointmentMemberId, Long appointmentOptionId) {}
}


static class FakeAppointmentOptionRepository {
AppointmentOption getById(Long id) { return null; }
}
Expand Down