Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,6 +23,7 @@ public class UserQueryController {

private final UserViewAliasChoiceUseCase userViewAliasChoiceUseCase;
private final UserGetFollowUsecase userGetFollowUsecase;
private final UserIsFollowingUsecase userIsFollowingUsecase;

/**
* 사용자 별칭 선택 화면 조회
Expand Down Expand Up @@ -51,4 +54,13 @@ public BaseResponse<UserFollowingResponse> 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<UserIsFollowingResponse> checkisFollowing(@UserId final Long userId,
@PathVariable final Long targetUserId) {
return BaseResponse.ok(UserIsFollowingResponse.of(userIsFollowingUsecase.isFollowing(userId, targetUserId)));
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,7 +30,7 @@ public class FollowingCommandPersistenceAdapter implements FollowingCommandPort
private final FollowingMapper followingMapper;
private final UserMapper userMapper;

@Override //ACTIVE, INACTIVE 모두 조회
@Override //ACTIVE만 조회
Comment thread
buzz0331 marked this conversation as resolved.
public Optional<Following> findByUserIdAndTargetUserId(Long userId, Long targetUserId) {
Optional<FollowingJpaEntity> followingJpaEntity = followingJpaRepository.findByUserAndTargetUser(userId, targetUserId);
return followingJpaEntity.map(followingMapper::toDomainEntity);
Expand All @@ -47,21 +47,21 @@ 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) {
UserJpaEntity userJpaEntity = userJpaRepository.findById(targetUser.getId()).orElseThrow(
() -> new EntityNotFoundException(USER_NOT_FOUND)
);

userJpaEntity.updateFollowerCount(targetUser.getFollowerCount());
userJpaEntity.updateFrom(targetUser);
userJpaRepository.save(userJpaEntity);
return userJpaEntity;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public Optional<FollowingJpaEntity> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface FollowDtoMapper {
public interface FollowQueryMapper {
Comment thread
buzz0331 marked this conversation as resolved.

UserFollowersResponse.Follower toFollowerList(FollowQueryDto dto);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package konkuk.thip.user.application.port.in;

public interface UserIsFollowingUsecase {
boolean isFollowing(Long userId, Long targetUserId);
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -9,7 +11,12 @@ public interface FollowingCommandPort {

Optional<Following> findByUserIdAndTargetUserId(Long userId, Long targetUserId);

default Following getByUserIdAndTargetUserIdOrThrow(Long userId, Long targetUserId) {
return findByUserIdAndTargetUserId(userId, targetUserId)
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.FOLLOW_NOT_FOUND));
}
Comment on lines 12 to +17
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM


void save(Following following, User targetUser);

void updateStatus(Following following, User targetUser);
void deleteFollowing(Following following, User targetUser);
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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)) {
Comment thread
buzz0331 marked this conversation as resolved.
throw new BusinessException(USER_CANNOT_FOLLOW_SELF);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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()
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -49,7 +51,7 @@ void tearDown() {
}

@Test
@DisplayName("팔로우 요청 후 언팔로우 요청 시 상태가 변경되는지 확인한다.")
@DisplayName("팔로우 요청 후 언팔로우 요청 시 엔티티가 삭제되었는지 확인한다.")
void changeFollowingState_follow_then_unfollow() throws Exception {
// 사용자 2명 저장
AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createScienceAlias());
Expand Down Expand Up @@ -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<FollowingJpaEntity> followingJpaEntityOptional = followingJpaRepository.findByUserAndTargetUser(followingUser.getUserId(), target.getUserId());
assertThat(followingJpaEntityOptional.isPresent()).isFalse();

userJpaEntity = userJpaRepository.findById(target.getUserId()).orElseThrow();
assertThat(userJpaEntity.getFollowerCount()).isEqualTo(0); // 팔로워 수 감소 확인
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading