diff --git a/src/main/java/team7/inplace/favoriteInfluencer/domain/FavoriteInfluencer.java b/src/main/java/team7/inplace/favoriteInfluencer/domain/FavoriteInfluencer.java index f78fb85c..5d0fa4f2 100644 --- a/src/main/java/team7/inplace/favoriteInfluencer/domain/FavoriteInfluencer.java +++ b/src/main/java/team7/inplace/favoriteInfluencer/domain/FavoriteInfluencer.java @@ -21,6 +21,7 @@ @NoArgsConstructor(access = PROTECTED) @Entity public class FavoriteInfluencer { + @Id @GeneratedValue(strategy = IDENTITY) private Long id; @@ -33,21 +34,9 @@ public class FavoriteInfluencer { @JoinColumn(name = "influencer_id") private Influencer influencer; @Column - private boolean like = false; - - public void check(boolean check) { - if (check) { - like(); - return; - } - dislike(); - } - - private void like() { - this.like = true; - } + private boolean isLiked = false; - private void dislike() { - this.like = false; + public void updateLike(boolean like) { + this.isLiked = like; } } diff --git a/src/main/java/team7/inplace/favoriteInfluencer/persistent/FavoriteInfluencerRepository.java b/src/main/java/team7/inplace/favoriteInfluencer/persistent/FavoriteInfluencerRepository.java index d6ad74c0..e850f167 100644 --- a/src/main/java/team7/inplace/favoriteInfluencer/persistent/FavoriteInfluencerRepository.java +++ b/src/main/java/team7/inplace/favoriteInfluencer/persistent/FavoriteInfluencerRepository.java @@ -1,9 +1,19 @@ package team7.inplace.favoriteInfluencer.persistent; import java.util.List; +import java.util.Optional; +import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import team7.inplace.favoriteInfluencer.domain.FavoriteInfluencer; public interface FavoriteInfluencerRepository extends JpaRepository { + List findByUserId(Long userId); + + Optional findByUserIdAndInfluencerId(Long userId, Long influencerId); + + @Query("SELECT f.influencer.id FROM FavoriteInfluencer f WHERE f.user.id = :userId AND f.isLiked = true") + Set findLikedInfluencerIdsByUserId(@Param("userId") Long userId); } diff --git a/src/main/java/team7/inplace/influencer/application/InfluencerService.java b/src/main/java/team7/inplace/influencer/application/InfluencerService.java index ec01ee77..d6238712 100644 --- a/src/main/java/team7/inplace/influencer/application/InfluencerService.java +++ b/src/main/java/team7/inplace/influencer/application/InfluencerService.java @@ -1,6 +1,7 @@ package team7.inplace.influencer.application; import java.util.List; +import java.util.Set; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,7 +14,7 @@ import team7.inplace.influencer.application.dto.InfluencerInfo; import team7.inplace.influencer.domain.Influencer; import team7.inplace.influencer.persistence.InfluencerRepository; -import team7.inplace.influencer.presentation.dto.InfluencerRequestParam; +import team7.inplace.influencer.presentation.dto.InfluencerLikeRequest; import team7.inplace.security.util.AuthorizationUtil; import team7.inplace.user.domain.User; import team7.inplace.user.persistence.UserRepository; @@ -28,9 +29,28 @@ public class InfluencerService { @Transactional(readOnly = true) public List getAllInfluencers() { - return influencerRepository.findAll().stream() - .map(InfluencerInfo::from) + List influencers = influencerRepository.findAll(); + Long userId = AuthorizationUtil.getUserId(); + + // 로그인 안된 경우, likes를 모두 false로 설정 + if (userId == null) { + return influencers.stream() + .map(influencer -> InfluencerInfo.from(influencer, false)) .toList(); + } + + // 로그인 된 경우 + Set likedInfluencerIds = favoriteRepository.findLikedInfluencerIdsByUserId(userId); + + List influencerInfos = influencers.stream() + .map(influencer -> { + boolean isLiked = likedInfluencerIds.contains(influencer.getId()); + return InfluencerInfo.from(influencer, isLiked); + }) + .sorted((a, b) -> Boolean.compare(b.likes(), a.likes())) + .toList(); + + return influencerInfos; } @Transactional @@ -43,7 +63,7 @@ public Long createInfluencer(InfluencerCommand command) { public Long updateInfluencer(Long id, InfluencerCommand command) { Influencer influencer = influencerRepository.findById(id).orElseThrow(); influencer.update(command.influencerName(), command.influencerImgUrl(), - command.influencerJob()); + command.influencerJob()); return influencer.getId(); } @@ -56,17 +76,20 @@ public void deleteInfluencer(Long id) { } @Transactional - public void likeToInfluencer(InfluencerRequestParam param) { + public void likeToInfluencer(InfluencerLikeRequest param) { String username = AuthorizationUtil.getUsername(); - if (StringUtils.hasText(username)) { + if (!StringUtils.hasText(username)) { throw InplaceException.of(AuthorizationErrorCode.TOKEN_IS_EMPTY); } User user = userRepository.findByUsername(username).orElseThrow(); Influencer influencer = influencerRepository.findById(param.influencerId()).orElseThrow(); - FavoriteInfluencer favorite = new FavoriteInfluencer(user, influencer); - favorite.check(param.likes()); + FavoriteInfluencer favorite = favoriteRepository.findByUserIdAndInfluencerId(user.getId(), + influencer.getId()) + .orElseGet(() -> new FavoriteInfluencer(user, influencer)); // 존재하지 않으면 새로 생성 + + favorite.updateLike(param.likes()); favoriteRepository.save(favorite); } } diff --git a/src/main/java/team7/inplace/influencer/application/dto/InfluencerInfo.java b/src/main/java/team7/inplace/influencer/application/dto/InfluencerInfo.java index b5e300ab..ca82957f 100644 --- a/src/main/java/team7/inplace/influencer/application/dto/InfluencerInfo.java +++ b/src/main/java/team7/inplace/influencer/application/dto/InfluencerInfo.java @@ -10,13 +10,13 @@ public record InfluencerInfo( boolean likes ) { - public static InfluencerInfo from(Influencer influencer) { + public static InfluencerInfo from(Influencer influencer, boolean isLiked) { return new InfluencerInfo( influencer.getId(), influencer.getName(), influencer.getImgUrl(), influencer.getJob(), - false // 좋아요 기능 추가할 때 로직 추가 예정 + isLiked ); } } diff --git a/src/main/java/team7/inplace/influencer/presentation/InfluencerController.java b/src/main/java/team7/inplace/influencer/presentation/InfluencerController.java index bcd4a545..5751c1ab 100644 --- a/src/main/java/team7/inplace/influencer/presentation/InfluencerController.java +++ b/src/main/java/team7/inplace/influencer/presentation/InfluencerController.java @@ -6,7 +6,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -16,9 +15,9 @@ import team7.inplace.influencer.application.InfluencerService; import team7.inplace.influencer.application.dto.InfluencerCommand; import team7.inplace.influencer.application.dto.InfluencerInfo; +import team7.inplace.influencer.presentation.dto.InfluencerLikeRequest; import team7.inplace.influencer.presentation.dto.InfluencerListResponse; import team7.inplace.influencer.presentation.dto.InfluencerRequest; -import team7.inplace.influencer.presentation.dto.InfluencerRequestParam; import team7.inplace.influencer.presentation.dto.InfluencerResponse; @RequiredArgsConstructor @@ -32,8 +31,8 @@ public class InfluencerController implements InfluencerControllerApiSpec { public ResponseEntity getAllInfluencers() { List influencersDtoList = influencerService.getAllInfluencers(); List influencers = influencersDtoList.stream() - .map(InfluencerResponse::from) - .toList(); + .map(InfluencerResponse::from) + .toList(); InfluencerListResponse response = new InfluencerListResponse(influencers); return new ResponseEntity<>(response, HttpStatus.OK); @@ -41,23 +40,21 @@ public ResponseEntity getAllInfluencers() { @PostMapping() public ResponseEntity createInfluencer(@RequestBody InfluencerRequest request) { - InfluencerCommand influencerCommand = new InfluencerCommand( - request.influencerName(), - request.influencerImgUrl(), - request.influencerJob() - ); + InfluencerCommand influencerCommand = InfluencerRequest.to(request); Long savedId = influencerService.createInfluencer(influencerCommand); return new ResponseEntity<>(savedId, HttpStatus.OK); } @PutMapping("/{id}") - public ResponseEntity updateInfluencer(@PathVariable Long id, - @RequestBody InfluencerRequest request) { + public ResponseEntity updateInfluencer( + @PathVariable Long id, + @RequestBody InfluencerRequest request + ) { InfluencerCommand influencerCommand = new InfluencerCommand( - request.influencerName(), - request.influencerImgUrl(), - request.influencerJob() + request.influencerName(), + request.influencerImgUrl(), + request.influencerJob() ); Long updatedId = influencerService.updateInfluencer(id, influencerCommand); @@ -72,7 +69,7 @@ public ResponseEntity deleteInfluencer(@PathVariable Long id) { } @PostMapping("/likes") - public ResponseEntity likeToInfluencer(@ModelAttribute InfluencerRequestParam param) { + public ResponseEntity likeToInfluencer(@RequestBody InfluencerLikeRequest param) { influencerService.likeToInfluencer(param); return new ResponseEntity<>(HttpStatus.OK); } diff --git a/src/main/java/team7/inplace/influencer/presentation/InfluencerControllerApiSpec.java b/src/main/java/team7/inplace/influencer/presentation/InfluencerControllerApiSpec.java index 71d8ccab..9ab1d4bc 100644 --- a/src/main/java/team7/inplace/influencer/presentation/InfluencerControllerApiSpec.java +++ b/src/main/java/team7/inplace/influencer/presentation/InfluencerControllerApiSpec.java @@ -4,12 +4,13 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; +import team7.inplace.influencer.presentation.dto.InfluencerLikeRequest; import team7.inplace.influencer.presentation.dto.InfluencerListResponse; import team7.inplace.influencer.presentation.dto.InfluencerRequest; public interface InfluencerControllerApiSpec { - @Operation(summary = "인플루언서들 리스트 반환", description = "토큰 유무에 따라 좋아요한 인플루언서 반영 여부가 다릅니다.") + @Operation(summary = "인플루언서들 리스트 반환", description = "토큰이 있는 경우 좋아요된 인플루언서가 먼저 반환됩니다.") ResponseEntity getAllInfluencers(); @Operation(summary = "인플루언서 등록", description = "새 인플루언서를 등록합니다.") @@ -21,4 +22,8 @@ ResponseEntity updateInfluencer(@PathVariable Long id, @Operation(summary = "인플루언서 삭제", description = "인플루언서를 삭제합니다.") ResponseEntity deleteInfluencer(@PathVariable Long id); + + @Operation(summary = "인플루언서 좋아요/좋아요 취소", description = "인플루언서를 좋아요하거나 좋아요 취소합니다.") + ResponseEntity likeToInfluencer(@RequestBody InfluencerLikeRequest param); + } diff --git a/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerLikeRequest.java b/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerLikeRequest.java new file mode 100644 index 00000000..c6e2024e --- /dev/null +++ b/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerLikeRequest.java @@ -0,0 +1,8 @@ +package team7.inplace.influencer.presentation.dto; + +public record InfluencerLikeRequest( + Long influencerId, + Boolean likes +) { + +} diff --git a/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerRequest.java b/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerRequest.java index 347dad96..a5ca7a26 100644 --- a/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerRequest.java +++ b/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerRequest.java @@ -1,9 +1,18 @@ package team7.inplace.influencer.presentation.dto; +import team7.inplace.influencer.application.dto.InfluencerCommand; + public record InfluencerRequest( String influencerName, String influencerImgUrl, String influencerJob ) { + public static InfluencerCommand to(InfluencerRequest request) { + return new InfluencerCommand( + request.influencerName(), + request.influencerImgUrl(), + request.influencerJob() + ); + } } diff --git a/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerRequestParam.java b/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerRequestParam.java deleted file mode 100644 index ba3544f6..00000000 --- a/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerRequestParam.java +++ /dev/null @@ -1,7 +0,0 @@ -package team7.inplace.influencer.presentation.dto; - -public record InfluencerRequestParam( - Long influencerId, - Boolean likes -) { -} diff --git a/src/test/java/team7/inplace/influencer/InfluencerRepositoryTest.java b/src/test/java/team7/inplace/influencer/InfluencerRepositoryTest.java index d56fda30..c9d6f897 100644 --- a/src/test/java/team7/inplace/influencer/InfluencerRepositoryTest.java +++ b/src/test/java/team7/inplace/influencer/InfluencerRepositoryTest.java @@ -5,11 +5,11 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.context.SpringBootTest; import team7.inplace.influencer.domain.Influencer; import team7.inplace.influencer.persistence.InfluencerRepository; -@DataJpaTest +@SpringBootTest public class InfluencerRepositoryTest { @Autowired @@ -17,16 +17,16 @@ public class InfluencerRepositoryTest { @Test public void findAllTest() { - Influencer influencer1 = new Influencer("influencer1", "imgUrl1", "job1"); - Influencer influencer2 = new Influencer("influencer2", "imgUrl2", "job2"); + Influencer influencer4 = new Influencer("influencer4", "imgUrl1", "job1"); + Influencer influencer5 = new Influencer("influencer5", "imgUrl2", "job2"); - influencerRepository.save(influencer1); - influencerRepository.save(influencer2); + influencerRepository.save(influencer4); + influencerRepository.save(influencer5); List savedInfluencers = influencerRepository.findAll(); - assertThat(savedInfluencers.get(0)).usingRecursiveComparison().isEqualTo(influencer1); - assertThat(savedInfluencers.get(1)).usingRecursiveComparison().isEqualTo(influencer2); + assertThat(savedInfluencers.get(3)).usingRecursiveComparison().isEqualTo(influencer4); + assertThat(savedInfluencers.get(4)).usingRecursiveComparison().isEqualTo(influencer5); } } diff --git a/src/test/java/team7/inplace/influencer/InfluencerServiceTest.java b/src/test/java/team7/inplace/influencer/InfluencerServiceTest.java index e25036b0..8b40eb03 100644 --- a/src/test/java/team7/inplace/influencer/InfluencerServiceTest.java +++ b/src/test/java/team7/inplace/influencer/InfluencerServiceTest.java @@ -4,22 +4,31 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; +import team7.inplace.favoriteInfluencer.domain.FavoriteInfluencer; +import team7.inplace.favoriteInfluencer.persistent.FavoriteInfluencerRepository; import team7.inplace.influencer.application.InfluencerService; import team7.inplace.influencer.application.dto.InfluencerCommand; import team7.inplace.influencer.application.dto.InfluencerInfo; import team7.inplace.influencer.domain.Influencer; import team7.inplace.influencer.persistence.InfluencerRepository; +import team7.inplace.security.util.AuthorizationUtil; +import team7.inplace.user.domain.Role; +import team7.inplace.user.domain.User; +import team7.inplace.user.domain.UserType; @ExtendWith(MockitoExtension.class) public class InfluencerServiceTest { @@ -27,15 +36,21 @@ public class InfluencerServiceTest { @Mock private InfluencerRepository influencerRepository; + @Mock + private FavoriteInfluencerRepository favoriteInfluencerRepository; + @InjectMocks private InfluencerService influencerService; @Test - public void getAllInfluencersTest() { + public void getAllInfluencersTest_NotLoggedIn() { + MockedStatic authorizationUtil = mockStatic(AuthorizationUtil.class); + Influencer influencer1 = new Influencer("influencer1", "imgUrl1", "job1"); Influencer influencer2 = new Influencer("influencer2", "imgUrl2", "job2"); given(influencerRepository.findAll()).willReturn(Arrays.asList(influencer1, influencer2)); + given(AuthorizationUtil.getUserId()).willReturn(null); List influencerInfoList = influencerService.getAllInfluencers(); @@ -44,8 +59,47 @@ public void getAllInfluencersTest() { assertThat(influencerInfoList.get(0).likes()).isFalse(); assertThat(influencerInfoList.get(1).influencerName()).isEqualTo("influencer2"); assertThat(influencerInfoList.get(1).likes()).isFalse(); + + authorizationUtil.close(); + } + + @Test + public void getAllInfluencersTest_LoggedIn() { + MockedStatic authorizationUtil = mockStatic(AuthorizationUtil.class); + + Influencer influencer1 = new Influencer(1L, "influencer1", "imgUrl1", "job1"); + Influencer influencer2 = new Influencer(2L, "influencer2", "imgUrl2", "job2"); + Influencer influencer3 = new Influencer(3L, "influencer3", "imgUrl3", "job3"); + + Long userId = 1L; + User user = new User("name", "password", "nickname", UserType.KAKAO, Role.USER); + + given(influencerRepository.findAll()).willReturn( + Arrays.asList(influencer1, influencer2, influencer3)); + given(AuthorizationUtil.getUserId()).willReturn(userId); + + // 2, 3번째 인플루언서 좋아요로 설정 + FavoriteInfluencer favoriteInfluencer1 = new FavoriteInfluencer(user, influencer2); + favoriteInfluencer1.updateLike(true); + FavoriteInfluencer favoriteInfluencer2 = new FavoriteInfluencer(user, influencer3); + favoriteInfluencer2.updateLike(true); + given(favoriteInfluencerRepository.findLikedInfluencerIdsByUserId(userId)).willReturn( + Set.of(2L, 3L)); + + List influencerInfoList = influencerService.getAllInfluencers(); + + assertThat(influencerInfoList).hasSize(3); + assertThat(influencerInfoList.get(0).influencerName()).isEqualTo("influencer2"); + assertThat(influencerInfoList.get(0).likes()).isTrue(); + assertThat(influencerInfoList.get(1).influencerName()).isEqualTo("influencer3"); + assertThat(influencerInfoList.get(1).likes()).isTrue(); + assertThat(influencerInfoList.get(2).influencerName()).isEqualTo("influencer1"); + assertThat(influencerInfoList.get(2).likes()).isFalse(); + + authorizationUtil.close(); } + @Test public void createInfluencerTest() { InfluencerCommand command = new InfluencerCommand("name", "imgUrl", "job");