diff --git a/src/main/java/es/princip/getp/domain/people/application/PeopleLikeService.java b/src/main/java/es/princip/getp/domain/people/application/PeopleLikeService.java index 57b2ba8d..49cabd97 100644 --- a/src/main/java/es/princip/getp/domain/people/application/PeopleLikeService.java +++ b/src/main/java/es/princip/getp/domain/people/application/PeopleLikeService.java @@ -11,6 +11,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -19,6 +21,14 @@ public class PeopleLikeService { private final ClientService clientService; private final PeopleLikeRepository peopleLikeRepository; + private PeopleLike get(Optional peopleLike) { + return peopleLike.orElseThrow(() -> new BusinessLogicException(PeopleLikeErrorCode.NEVER_LIKED)); + } + + public PeopleLike getByMemberIdAndPeopleId(Long memberId, Long peopleId) { + return get(peopleLikeRepository.findByPeople_PeopleIdAndClient_ClientId(memberId, peopleId)); + } + private void checkPeopleIsAlreadyLiked(Long clientId, Long peopleId) { if (peopleLikeRepository.existsByClient_ClientIdAndPeople_PeopleId(clientId, peopleId)) { throw new BusinessLogicException(PeopleLikeErrorCode.ALREADY_LIKED); @@ -37,4 +47,10 @@ public void like(Long memberId, Long peopleId) { .build() ); } + + @Transactional + public void unlike(Long memberId, Long peopleId) { + PeopleLike peopleLike = getByMemberIdAndPeopleId(memberId, peopleId); + peopleLikeRepository.delete(peopleLike); + } } diff --git a/src/main/java/es/princip/getp/domain/people/exception/PeopleLikeErrorCode.java b/src/main/java/es/princip/getp/domain/people/exception/PeopleLikeErrorCode.java index 4d9b9c7f..facb06d7 100644 --- a/src/main/java/es/princip/getp/domain/people/exception/PeopleLikeErrorCode.java +++ b/src/main/java/es/princip/getp/domain/people/exception/PeopleLikeErrorCode.java @@ -1,12 +1,12 @@ package es.princip.getp.domain.people.exception; -import org.springframework.http.HttpStatus; - import es.princip.getp.infra.exception.ErrorCode; import es.princip.getp.infra.exception.ErrorDescription; +import org.springframework.http.HttpStatus; public enum PeopleLikeErrorCode implements ErrorCode { - ALREADY_LIKED(HttpStatus.CONFLICT, "이미 좋아요를 누른 피플입니다."); + ALREADY_LIKED(HttpStatus.CONFLICT, "이미 좋아요를 누른 피플입니다."), + NEVER_LIKED(HttpStatus.NOT_FOUND, "해당 피플에게 좋아요를 누른 적이 없습니다."); private final HttpStatus status; private final ErrorDescription description; diff --git a/src/main/java/es/princip/getp/domain/people/presentation/PeopleLikeController.java b/src/main/java/es/princip/getp/domain/people/presentation/PeopleLikeController.java index 94e1c124..e1fb8707 100644 --- a/src/main/java/es/princip/getp/domain/people/presentation/PeopleLikeController.java +++ b/src/main/java/es/princip/getp/domain/people/presentation/PeopleLikeController.java @@ -9,10 +9,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/people") @@ -37,4 +34,21 @@ public ResponseEntity> like( return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.success(HttpStatus.CREATED)); } + + /** + * 피플 좋아요 취소 + * + * @param peopleId 좋아요를 취소할 피플 ID + * @param principalDetails 로그인한 사용자 정보 + */ + @DeleteMapping("/{peopleId}/likes") + @PreAuthorize("hasRole('CLIENT') and isAuthenticated()") + public ResponseEntity> unlike( + @PathVariable Long peopleId, + @AuthenticationPrincipal PrincipalDetails principalDetails + ) { + peopleLikeService.unlike(principalDetails.getMember().getMemberId(), peopleId); + return ResponseEntity.status(HttpStatus.NO_CONTENT) + .body(ApiResponse.success(HttpStatus.NO_CONTENT)); + } } diff --git a/src/main/java/es/princip/getp/domain/people/repository/PeopleLikeRepository.java b/src/main/java/es/princip/getp/domain/people/repository/PeopleLikeRepository.java index bbef83b9..c9147825 100644 --- a/src/main/java/es/princip/getp/domain/people/repository/PeopleLikeRepository.java +++ b/src/main/java/es/princip/getp/domain/people/repository/PeopleLikeRepository.java @@ -3,7 +3,11 @@ import es.princip.getp.domain.people.domain.PeopleLike; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface PeopleLikeRepository extends JpaRepository { boolean existsByClient_ClientIdAndPeople_PeopleId(Long clientId, Long peopleId); + + Optional findByPeople_PeopleIdAndClient_ClientId(Long peopleId, Long clientId); } \ No newline at end of file diff --git a/src/test/java/es/princip/getp/domain/people/application/PeopleLikeServiceTest.java b/src/test/java/es/princip/getp/domain/people/application/PeopleLikeServiceTest.java index fef338c7..af7a8a9e 100644 --- a/src/test/java/es/princip/getp/domain/people/application/PeopleLikeServiceTest.java +++ b/src/test/java/es/princip/getp/domain/people/application/PeopleLikeServiceTest.java @@ -4,6 +4,7 @@ import es.princip.getp.domain.client.domain.Client; import es.princip.getp.domain.member.domain.Member; import es.princip.getp.domain.people.domain.People; +import es.princip.getp.domain.people.domain.PeopleLike; import es.princip.getp.domain.people.exception.PeopleErrorCode; import es.princip.getp.domain.people.exception.PeopleLikeErrorCode; import es.princip.getp.domain.people.repository.PeopleLikeRepository; @@ -16,9 +17,12 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.Optional; + import static es.princip.getp.domain.client.fixture.ClientFixture.createClient; import static es.princip.getp.domain.member.fixture.MemberFixture.createMember; import static es.princip.getp.domain.people.fixture.PeopleFixture.createPeople; +import static es.princip.getp.domain.people.fixture.PeopleLikeFixture.createPeopleLike; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.BDDAssertions.catchThrowableOfType; import static org.mockito.ArgumentMatchers.any; @@ -51,6 +55,7 @@ class Like { private final People people = createPeople(member); private final Client client = createClient(); + private final PeopleLike peopleLike = createPeopleLike(client, people); @DisplayName("피플에게 좋아요를 누른다.") @Test @@ -94,5 +99,28 @@ void like_WhenPeopleIsNotFound_ShouldThrowException() { BusinessLogicException.class); assertThat(exception.getErrorCode()).isEqualTo(PeopleErrorCode.PEOPLE_NOT_FOUND); } + + @DisplayName("피플에게 좋아요를 취소한다.") + @Test + void unlike() { + given(peopleLikeRepository.findByPeople_PeopleIdAndClient_ClientId(memberId, peopleId)).willReturn(Optional.of(peopleLike)); + + peopleLikeService.unlike(memberId, peopleId); + + verify(peopleLikeRepository, times(1)).delete(peopleLike); + } + + @DisplayName("좋아요를 누르지 않은 피플에게 의뢰자가 좋아요를 취소할 경우 예외를 던진다.") + @Test + void unlike_ThrowExceptionWhenLikerCancelsLikeForPeopleNotLiked() { + given(peopleLikeRepository.findByPeople_PeopleIdAndClient_ClientId(peopleId, memberId)).willThrow( + new BusinessLogicException(PeopleLikeErrorCode.NEVER_LIKED) + ); + + BusinessLogicException exception = + catchThrowableOfType(() -> peopleLikeService.unlike(memberId, peopleId), + BusinessLogicException.class); + assertThat(exception.getErrorCode()).isEqualTo(PeopleLikeErrorCode.NEVER_LIKED); + } } } \ No newline at end of file diff --git a/src/test/java/es/princip/getp/domain/people/fixture/PeopleLikeFixture.java b/src/test/java/es/princip/getp/domain/people/fixture/PeopleLikeFixture.java new file mode 100644 index 00000000..fd76864a --- /dev/null +++ b/src/test/java/es/princip/getp/domain/people/fixture/PeopleLikeFixture.java @@ -0,0 +1,14 @@ +package es.princip.getp.domain.people.fixture; + +import es.princip.getp.domain.client.domain.Client; +import es.princip.getp.domain.people.domain.People; +import es.princip.getp.domain.people.domain.PeopleLike; + +public class PeopleLikeFixture { + public static PeopleLike createPeopleLike(final Client client, final People people) { + return PeopleLike.builder() + .client(client) + .people(people) + .build(); + } +} diff --git a/src/test/java/es/princip/getp/domain/people/presentation/PeopleLikeControllerTest.java b/src/test/java/es/princip/getp/domain/people/presentation/PeopleLikeControllerTest.java index b36965b4..b8417a14 100644 --- a/src/test/java/es/princip/getp/domain/people/presentation/PeopleLikeControllerTest.java +++ b/src/test/java/es/princip/getp/domain/people/presentation/PeopleLikeControllerTest.java @@ -13,13 +13,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import static es.princip.getp.domain.member.domain.MemberType.ROLE_CLIENT; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.willDoNothing; import static org.mockito.BDDMockito.willThrow; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest({PeopleLikeController.class, PeopleErrorCodeController.class}) @@ -37,23 +38,25 @@ class Like { private final Long peopleId = 1L; @DisplayName("의뢰자는 피플에게 좋아요를 누를 수 있다.") - @WithCustomMockUser(memberType = ROLE_CLIENT) + @WithCustomMockUser(memberType = MemberType.ROLE_CLIENT) @Test void like() throws Exception { willDoNothing().given(peopleLikeService).like(any(), eq(peopleId)); - mockMvc.perform(post("/people/{peopleId}/likes", peopleId)) + mockMvc.perform(post("/people/{peopleId}/likes", peopleId) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isCreated()); } - @DisplayName("의뢰자는 이미 좋아요를 누른 피플에게 다시 좋아요를 누를 수 없다.") - @WithCustomMockUser(memberType = ROLE_CLIENT) + @DisplayName("의뢰자는 피플에게 중복으로 좋아요를 누를 수 없다.") + @WithCustomMockUser(memberType = MemberType.ROLE_CLIENT) @Test void like_WhenPeopleIsAlreadyLiked_ShouldBeFailed() throws Exception { willThrow(new BusinessLogicException(PeopleLikeErrorCode.ALREADY_LIKED)) .given(peopleLikeService).like(any(), eq(peopleId)); - mockMvc.perform(post("/people/{peopleId}/likes", peopleId)) + mockMvc.perform(post("/people/{peopleId}/likes", peopleId) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isConflict()); } @@ -61,19 +64,47 @@ void like_WhenPeopleIsAlreadyLiked_ShouldBeFailed() throws Exception { @WithCustomMockUser(memberType = MemberType.ROLE_PEOPLE) @Test void like_WhenMemberTypeIsPeople_ShouldBeFailed() throws Exception { - mockMvc.perform(post("/people/{peopleId}/likes", peopleId)) + mockMvc.perform(post("/people/{peopleId}/likes", peopleId) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isForbidden()); } @DisplayName("의뢰자는 존재하지 않는 피플에게 좋아요를 누를 수 없다.") - @WithCustomMockUser(memberType = ROLE_CLIENT) + @WithCustomMockUser(memberType = MemberType.ROLE_CLIENT) @Test void like_WhenPeopleIsNotFound_ShouldBeFailed() throws Exception { willThrow(new BusinessLogicException(PeopleErrorCode.PEOPLE_NOT_FOUND)) .given(peopleLikeService).like(any(), eq(peopleId)); - mockMvc.perform(post("/people/{peopleId}/likes", peopleId)) + mockMvc.perform(post("/people/{peopleId}/likes", peopleId) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()); } } + + @Nested + class Unlike { + + private final Long peopleId = 1L; + + @DisplayName("의뢰자는 피플에게 눌렀던 좋아요를 취소를 할 수 있다.") + @WithCustomMockUser(memberType = MemberType.ROLE_CLIENT) + @Test + void unlike() throws Exception { + willDoNothing().given(peopleLikeService).unlike(any(), eq(peopleId)); + + mockMvc.perform(delete("/people/{peopleId}/likes", peopleId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + } + + @DisplayName("피플은 피플에게 좋아요를 취소할 수 없다.") + @WithCustomMockUser(memberType = MemberType.ROLE_PEOPLE) + @Test + void unlike_WhenMemberTypeIsPeople_ShouldBeFailed() throws Exception { + mockMvc.perform(delete("/people/{peopleId}/likes", peopleId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + } } \ No newline at end of file