diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java index 8a2454f17..2fd599ebd 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java @@ -6,8 +6,10 @@ import konkuk.thip.common.security.annotation.UserId; import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse; import konkuk.thip.user.adapter.in.web.response.UserFollowingResponse; +import konkuk.thip.user.adapter.in.web.response.UserIsFollowingResponse; import konkuk.thip.user.adapter.in.web.response.UserViewAliasChoiceResponse; import konkuk.thip.user.application.port.in.UserGetFollowUsecase; +import konkuk.thip.user.application.port.in.UserIsFollowingUsecase; import konkuk.thip.user.application.port.in.UserViewAliasChoiceUseCase; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; @@ -21,6 +23,7 @@ public class UserQueryController { private final UserViewAliasChoiceUseCase userViewAliasChoiceUseCase; private final UserGetFollowUsecase userGetFollowUsecase; + private final UserIsFollowingUsecase userIsFollowingUsecase; /** * 사용자 별칭 선택 화면 조회 @@ -51,4 +54,13 @@ public BaseResponse showMyFollowing(@UserId final Long us @RequestParam(defaultValue = "10") @Max(value = 10) @Min(value = 1) final int size) { return BaseResponse.ok(userGetFollowUsecase.getMyFollowing(userId, cursor, size)); } + + /** + * 팔로잉 여부 조회 + */ + @GetMapping("/users/{targetUserId}/is-following") + public BaseResponse checkisFollowing(@UserId final Long userId, + @PathVariable final Long targetUserId) { + return BaseResponse.ok(UserIsFollowingResponse.of(userIsFollowingUsecase.isFollowing(userId, targetUserId))); + } } diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/response/UserIsFollowingResponse.java b/src/main/java/konkuk/thip/user/adapter/in/web/response/UserIsFollowingResponse.java new file mode 100644 index 000000000..a6cc7f343 --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/in/web/response/UserIsFollowingResponse.java @@ -0,0 +1,9 @@ +package konkuk.thip.user.adapter.in.web.response; + +public record UserIsFollowingResponse( + boolean isFollowing +) { + public static UserIsFollowingResponse of(boolean isFollowing) { + return new UserIsFollowingResponse(isFollowing); + } +} diff --git a/src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java b/src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java index 07767f6a7..b1836276f 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java +++ b/src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java @@ -3,12 +3,10 @@ import jakarta.persistence.*; import konkuk.thip.common.entity.BaseJpaEntity; import lombok.*; -import org.hibernate.annotations.SQLDelete; @Entity @Table(name = "followings") @Getter -@SQLDelete(sql = "UPDATE followings SET status = 'INACTIVE' WHERE following_id = ?") @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder diff --git a/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java b/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java index 47ec8d4ed..a1fdd3bdf 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java +++ b/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java @@ -3,7 +3,9 @@ import jakarta.persistence.*; import konkuk.thip.common.entity.BaseJpaEntity; +import konkuk.thip.user.domain.User; import lombok.*; +import org.springframework.util.Assert; @Entity @Table(name = "users") @@ -38,8 +40,11 @@ public class UserJpaEntity extends BaseJpaEntity { @JoinColumn(name = "user_alias_id", nullable = false) private AliasJpaEntity aliasForUserJpaEntity; - public void updateFollowerCount(int followerCount) { - this.followerCount = followerCount; + public void updateFrom(User user) { + this.nickname = user.getNickname(); + Assert.notNull(user.getAlias(), "Alias must not be null"); + this.imageUrl = user.getAlias().getImageUrl(); + this.role = UserRole.from(user.getUserRole()); + this.followerCount = user.getFollowerCount(); } - } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java index e7025639a..c56ad147c 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java @@ -5,8 +5,8 @@ import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.mapper.FollowingMapper; import konkuk.thip.user.adapter.out.mapper.UserMapper; -import konkuk.thip.user.adapter.out.persistence.repository.alias.AliasJpaRepository; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.adapter.out.persistence.repository.alias.AliasJpaRepository; import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import konkuk.thip.user.application.port.out.FollowingCommandPort; import konkuk.thip.user.domain.Following; @@ -30,7 +30,7 @@ public class FollowingCommandPersistenceAdapter implements FollowingCommandPort private final FollowingMapper followingMapper; private final UserMapper userMapper; - @Override //ACTIVE, INACTIVE 모두 조회 + @Override //ACTIVE만 조회 public Optional findByUserIdAndTargetUserId(Long userId, Long targetUserId) { Optional followingJpaEntity = followingJpaRepository.findByUserAndTargetUser(userId, targetUserId); return followingJpaEntity.map(followingMapper::toDomainEntity); @@ -47,13 +47,13 @@ public void save(Following following, User targetUser) { // insert용 } @Override - public void updateStatus(Following following, User targetUser) { // 상태변경 용 + public void deleteFollowing(Following following, User targetUser) { updateUserFollowerCount(targetUser); - FollowingJpaEntity entity = followingJpaRepository.findByUserAndTargetUser(following.getUserId(), following.getFollowingUserId()) + FollowingJpaEntity followingJpaEntity = followingJpaRepository.findByUserAndTargetUser(following.getUserId(), following.getFollowingUserId()) .orElseThrow(() -> new EntityNotFoundException(FOLLOW_NOT_FOUND)); - entity.setStatus(following.getStatus()); + followingJpaRepository.delete(followingJpaEntity); } private UserJpaEntity updateUserFollowerCount(User targetUser) { @@ -61,7 +61,7 @@ private UserJpaEntity updateUserFollowerCount(User targetUser) { () -> new EntityNotFoundException(USER_NOT_FOUND) ); - userJpaEntity.updateFollowerCount(targetUser.getFollowerCount()); + userJpaEntity.updateFrom(targetUser); userJpaRepository.save(userJpaEntity); return userJpaEntity; } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java index 68dc546c8..dae546e69 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java @@ -29,7 +29,8 @@ public Optional findByUserAndTargetUser(Long userId, Long ta FollowingJpaEntity followingJpaEntity = jpaQueryFactory .selectFrom(following) .where(following.userJpaEntity.userId.eq(userId) - .and(following.followingUserJpaEntity.userId.eq(targetUserId))) + .and(following.followingUserJpaEntity.userId.eq(targetUserId)) + .and(following.status.eq(StatusType.ACTIVE))) .fetchOne(); return Optional.ofNullable(followingJpaEntity); diff --git a/src/main/java/konkuk/thip/user/application/mapper/FollowDtoMapper.java b/src/main/java/konkuk/thip/user/application/mapper/FollowQueryMapper.java similarity index 92% rename from src/main/java/konkuk/thip/user/application/mapper/FollowDtoMapper.java rename to src/main/java/konkuk/thip/user/application/mapper/FollowQueryMapper.java index 8c29f16b2..ce01eea4d 100644 --- a/src/main/java/konkuk/thip/user/application/mapper/FollowDtoMapper.java +++ b/src/main/java/konkuk/thip/user/application/mapper/FollowQueryMapper.java @@ -6,7 +6,7 @@ import org.mapstruct.Mapper; @Mapper(componentModel = "spring") -public interface FollowDtoMapper { +public interface FollowQueryMapper { UserFollowersResponse.Follower toFollowerList(FollowQueryDto dto); diff --git a/src/main/java/konkuk/thip/user/application/port/in/UserIsFollowingUsecase.java b/src/main/java/konkuk/thip/user/application/port/in/UserIsFollowingUsecase.java new file mode 100644 index 000000000..27649086d --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/port/in/UserIsFollowingUsecase.java @@ -0,0 +1,5 @@ +package konkuk.thip.user.application.port.in; + +public interface UserIsFollowingUsecase { + boolean isFollowing(Long userId, Long targetUserId); +} diff --git a/src/main/java/konkuk/thip/user/application/port/out/FollowingCommandPort.java b/src/main/java/konkuk/thip/user/application/port/out/FollowingCommandPort.java index 9b423c452..c126b73ce 100644 --- a/src/main/java/konkuk/thip/user/application/port/out/FollowingCommandPort.java +++ b/src/main/java/konkuk/thip/user/application/port/out/FollowingCommandPort.java @@ -1,5 +1,7 @@ package konkuk.thip.user.application.port.out; +import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.common.exception.code.ErrorCode; import konkuk.thip.user.domain.Following; import konkuk.thip.user.domain.User; @@ -9,7 +11,12 @@ public interface FollowingCommandPort { Optional findByUserIdAndTargetUserId(Long userId, Long targetUserId); + default Following getByUserIdAndTargetUserIdOrThrow(Long userId, Long targetUserId) { + return findByUserIdAndTargetUserId(userId, targetUserId) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.FOLLOW_NOT_FOUND)); + } + void save(Following following, User targetUser); - void updateStatus(Following following, User targetUser); + void deleteFollowing(Following following, User targetUser); } diff --git a/src/main/java/konkuk/thip/user/application/service/UserIsFollowingService.java b/src/main/java/konkuk/thip/user/application/service/UserIsFollowingService.java new file mode 100644 index 000000000..7a036c95b --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/service/UserIsFollowingService.java @@ -0,0 +1,19 @@ +package konkuk.thip.user.application.service; + +import konkuk.thip.user.application.port.in.UserIsFollowingUsecase; +import konkuk.thip.user.application.port.out.FollowingCommandPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserIsFollowingService implements UserIsFollowingUsecase { + + private final FollowingCommandPort followingCommandPort; + + @Override + public boolean isFollowing(Long userId, Long targetUserId) { + return followingCommandPort.findByUserIdAndTargetUserId(userId, targetUserId) + .isPresent(); + } +} diff --git a/src/main/java/konkuk/thip/user/application/service/following/UserFollowService.java b/src/main/java/konkuk/thip/user/application/service/following/UserFollowService.java index 40eeba525..c3be511e5 100644 --- a/src/main/java/konkuk/thip/user/application/service/following/UserFollowService.java +++ b/src/main/java/konkuk/thip/user/application/service/following/UserFollowService.java @@ -11,7 +11,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Objects; import java.util.Optional; import static konkuk.thip.common.exception.code.ErrorCode.USER_ALREADY_UNFOLLOWED; @@ -40,7 +39,7 @@ public Boolean changeFollowingState(UserFollowCommand followCommand) { Following following = optionalFollowing.get(); boolean isFollowing = following.changeFollowingState(type); targetUser.updateFollowerCount(isFollowing); - followingCommandPort.updateStatus(following, targetUser); + followingCommandPort.deleteFollowing(following, targetUser); return isFollowing; } else { // 팔로우 관계가 존재하지 않는 경우 if (!type) { @@ -53,7 +52,7 @@ public Boolean changeFollowingState(UserFollowCommand followCommand) { } private void validateParams(Long userId, Long targetUserId) { - if(Objects.equals(userId, targetUserId)) { + if(userId.equals(targetUserId)) { throw new BusinessException(USER_CANNOT_FOLLOW_SELF); } } diff --git a/src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java b/src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java index 24930a715..3991df5d4 100644 --- a/src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java +++ b/src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java @@ -3,7 +3,7 @@ import konkuk.thip.common.util.CursorBasedList; import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse; import konkuk.thip.user.adapter.in.web.response.UserFollowingResponse; -import konkuk.thip.user.application.mapper.FollowDtoMapper; +import konkuk.thip.user.application.mapper.FollowQueryMapper; import konkuk.thip.user.application.port.out.dto.FollowQueryDto; import konkuk.thip.user.application.port.in.UserGetFollowUsecase; import konkuk.thip.user.application.port.out.FollowingQueryPort; @@ -20,7 +20,7 @@ public class UserGetFollowService implements UserGetFollowUsecase { private final FollowingQueryPort followingQueryPort; private final UserCommandPort userCommandPort; - private final FollowDtoMapper followDtoMapper; + private final FollowQueryMapper followQueryMapper; private static final int MAX_PAGE_SIZE = 10; @@ -34,7 +34,7 @@ public UserFollowersResponse getUserFollowers(Long userId, String cursor, int si ); var followers = result.contents().stream() - .map(followDtoMapper::toFollowerList) + .map(followQueryMapper::toFollowerList) .toList(); return UserFollowersResponse.builder() @@ -54,7 +54,7 @@ public UserFollowingResponse getMyFollowing(Long userId, String cursor, int size ); var following = result.contents().stream() - .map(followDtoMapper::toFollowingList) + .map(followQueryMapper::toFollowingList) .toList(); return UserFollowingResponse.builder() diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java index 130eee046..f4dba60cb 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java @@ -5,8 +5,8 @@ import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserRole; -import konkuk.thip.user.adapter.out.persistence.repository.alias.AliasJpaRepository; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.adapter.out.persistence.repository.alias.AliasJpaRepository; import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -18,6 +18,8 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import java.util.Optional; + import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -49,7 +51,7 @@ void tearDown() { } @Test - @DisplayName("팔로우 요청 후 언팔로우 요청 시 상태가 변경되는지 확인한다.") + @DisplayName("팔로우 요청 후 언팔로우 요청 시 엔티티가 삭제되었는지 확인한다.") void changeFollowingState_follow_then_unfollow() throws Exception { // 사용자 2명 저장 AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createScienceAlias()); @@ -93,9 +95,9 @@ void changeFollowingState_follow_then_unfollow() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.data.isFollowing").value(false)); - // DB에 상태가 INACTIVE로 변경되었는지 확인 - FollowingJpaEntity updatedEntity = followingJpaRepository.findByUserAndTargetUser(followingUser.getUserId(), target.getUserId()).orElseThrow(); - assertThat(updatedEntity.getStatus().name()).isEqualTo("INACTIVE"); + // DB에서 삭제되었는지 확인 + Optional followingJpaEntityOptional = followingJpaRepository.findByUserAndTargetUser(followingUser.getUserId(), target.getUserId()); + assertThat(followingJpaEntityOptional.isPresent()).isFalse(); userJpaEntity = userJpaRepository.findById(target.getUserId()).orElseThrow(); assertThat(userJpaEntity.getFollowerCount()).isEqualTo(0); // 팔로워 수 감소 확인 diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserIsFollowingApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserIsFollowingApiTest.java new file mode 100644 index 000000000..5519fe07e --- /dev/null +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserIsFollowingApiTest.java @@ -0,0 +1,90 @@ +package konkuk.thip.user.adapter.in.web; + +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; +import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.adapter.out.persistence.repository.alias.AliasJpaRepository; +import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 팔로잉 여부 조회 API 통합 테스트") +class UserIsFollowingApiTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private UserJpaRepository userJpaRepository; + + @Autowired + private AliasJpaRepository aliasJpaRepository; + + @Autowired + private FollowingJpaRepository followingJpaRepository; + + @AfterEach + void tearDown() { + followingJpaRepository.deleteAllInBatch(); + userJpaRepository.deleteAll(); + aliasJpaRepository.deleteAll(); + } + + @Test + @DisplayName("팔로우 관계가 존재하면 true를 반환한다.") + void isFollowing_true() throws Exception { + // given + AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createScienceAlias()); + + UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(alias)); + + UserJpaEntity target = userJpaRepository.save(TestEntityFactory.createUser(alias)); + + // 팔로잉 관계 저장 + followingJpaRepository.save(FollowingJpaEntity.builder() + .userJpaEntity(user) + .followingUserJpaEntity(target) + .build()); + + // when & then + mockMvc.perform(get("/users/{targetUserId}/is-following", target.getUserId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.isFollowing").value(true)); + } + + @Test + @DisplayName("팔로우 관계가 없으면 false를 반환한다.") + void isFollowing_false() throws Exception { + // given + AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createScienceAlias()); + + UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(alias)); + + UserJpaEntity target = userJpaRepository.save(TestEntityFactory.createUser(alias)); + + // when & then + mockMvc.perform(get("/users/{targetUserId}/is-following", target.getUserId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.isFollowing").value(false)); + } +} \ No newline at end of file diff --git a/src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java b/src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java index 3ac9943fa..706bba7ad 100644 --- a/src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java +++ b/src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java @@ -66,7 +66,7 @@ void activate_existingFollowing() { assertThat(result).isTrue(); assertThat(inactiveFollowing.getStatus()).isEqualTo(StatusType.ACTIVE); assertThat(user.getFollowerCount()).isEqualTo(1); // followerCount 증가 확인 - verify(followingCommandPort).updateStatus(inactiveFollowing, user); + verify(followingCommandPort).deleteFollowing(inactiveFollowing, user); } @Test @@ -130,7 +130,7 @@ void deactivate_existingFollowing() { assertThat(result).isFalse(); assertThat(activeFollowing.getStatus()).isEqualTo(StatusType.INACTIVE); assertThat(user.getFollowerCount()).isEqualTo(0); // followerCount 감소 확인 - verify(followingCommandPort).updateStatus(activeFollowing, user); + verify(followingCommandPort).deleteFollowing(activeFollowing, user); } @Test