Skip to content

Commit

Permalink
feat: 나와 타인의 팔로우/팔로잉 카운트 조회 (#199)
Browse files Browse the repository at this point in the history
* refactor: MemberRelation 필드 source terget으로 변경

* feat: 나와 타인의 팔로우/팔로잉 카운트 조회 구현

* refactor: MethodArgumentTypeMismatchException Exception 주석과 에러내용 변경

* test: 나와 타인의 팔로우/팔로잉 카운트 조회 테스트코드 작성

* refactor: FollowStatus value 변경

* fix: enum 응답 값 JsonFormat 삭제
  • Loading branch information
kdomo authored Jan 23, 2024
1 parent 8ce7c9d commit f88d861
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.depromeet.domain.follow.application.FollowService;
import com.depromeet.domain.follow.dto.request.FollowCreateRequest;
import com.depromeet.domain.follow.dto.request.FollowDeleteRequest;
import com.depromeet.domain.follow.dto.response.FollowFindMeInfoResponse;
import com.depromeet.domain.follow.dto.response.FollowFindTargetInfoResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
Expand Down Expand Up @@ -30,4 +32,18 @@ public ResponseEntity<Void> followCreate(@Valid @RequestBody FollowCreateRequest
public void followDelete(@Valid @RequestBody FollowDeleteRequest request) {
followService.deleteFollow(request);
}

@GetMapping("/{targetId}")
@Operation(
summary = "타인의 팔로우 카운트 확인",
description = "타인의 팔로잉/팔로워 카운트와 내가 타인을 팔로우를 하고있는지 확인합니다.")
public FollowFindTargetInfoResponse followFindTarget(@PathVariable Long targetId) {
return followService.findTargetFollowInfo(targetId);
}

@GetMapping("/me")
@Operation(summary = "나의 팔로우 카운트 확인", description = "나의 팔로잉/팔로워 카운트를 확인합니다.")
public FollowFindMeInfoResponse followFindMe() {
return followService.findMeFollowInfo();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import com.depromeet.domain.follow.domain.MemberRelation;
import com.depromeet.domain.follow.dto.request.FollowCreateRequest;
import com.depromeet.domain.follow.dto.request.FollowDeleteRequest;
import com.depromeet.domain.follow.dto.response.FollowFindMeInfoResponse;
import com.depromeet.domain.follow.dto.response.FollowFindTargetInfoResponse;
import com.depromeet.domain.follow.dto.response.FollowStatus;
import com.depromeet.domain.member.dao.MemberRepository;
import com.depromeet.domain.member.domain.Member;
import com.depromeet.global.error.exception.CustomException;
Expand All @@ -26,7 +29,7 @@ public void createFollow(FollowCreateRequest request) {
Member targetMember = getTargetMember(request.targetId());

boolean existMemberRelation =
memberRelationRepository.existsByFollowerIdAndFollowingId(
memberRelationRepository.existsBySourceIdAndTargetId(
currentMember.getId(), targetMember.getId());
if (existMemberRelation) {
throw new CustomException(ErrorCode.FOLLOW_ALREADY_EXIST);
Expand All @@ -43,12 +46,49 @@ public void deleteFollow(FollowDeleteRequest request) {

MemberRelation memberRelation =
memberRelationRepository
.findByFollowerIdAndFollowingId(currentMember.getId(), targetMember.getId())
.findBySourceIdAndTargetId(currentMember.getId(), targetMember.getId())
.orElseThrow(() -> new CustomException(ErrorCode.FOLLOW_NOT_EXIST));

memberRelationRepository.delete(memberRelation);
}

public FollowFindTargetInfoResponse findTargetFollowInfo(Long targetId) {
final Member currentMember = memberUtil.getCurrentMember();
final Member targetMember = getTargetMember(targetId);

Long followingCount = memberRelationRepository.countBySourceId(targetMember.getId());
Long followerCount = memberRelationRepository.countByTargetId(targetMember.getId());

boolean isFollowing =
memberRelationRepository.existsBySourceIdAndTargetId(
currentMember.getId(), targetMember.getId());
boolean isFollowedByMe =
memberRelationRepository.existsBySourceIdAndTargetId(
targetMember.getId(), currentMember.getId());
FollowStatus followStatus = determineFollowStatus(isFollowing, isFollowedByMe);

return FollowFindTargetInfoResponse.of(followingCount, followerCount, followStatus);
}

public FollowFindMeInfoResponse findMeFollowInfo() {
final Member currentMember = memberUtil.getCurrentMember();

Long followingCount = memberRelationRepository.countBySourceId(currentMember.getId());
Long followerCount = memberRelationRepository.countByTargetId(currentMember.getId());

return FollowFindMeInfoResponse.of(followingCount, followerCount);
}

private FollowStatus determineFollowStatus(boolean isFollowing, boolean isFollowedByMe) {
if (!isFollowing) {
if (isFollowedByMe) {
return FollowStatus.FOLLOWED_BY_ME;
}
return FollowStatus.NOT_FOLLOWING;
}
return FollowStatus.FOLLOWING;
}

private Member getTargetMember(Long targetId) {
Member targetMember =
memberRepository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRelationRepository extends JpaRepository<MemberRelation, Long> {
Optional<MemberRelation> findByFollowerIdAndFollowingId(Long followerId, Long followingId);
Optional<MemberRelation> findBySourceIdAndTargetId(Long sourceId, Long targetId);

boolean existsByFollowerIdAndFollowingId(Long followerId, Long followingId);
boolean existsBySourceIdAndTargetId(Long sourceId, Long targetId);

Long countBySourceId(Long sourceId);

Long countByTargetId(Long targetId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
uniqueConstraints = {
@UniqueConstraint(
name = "member_relation_uk",
columnNames = {"follower_id", "following_id"})
columnNames = {"source_id", "target_id"})
})
public class MemberRelation extends BaseTimeEntity {
@Id
Expand All @@ -32,21 +32,21 @@ public class MemberRelation extends BaseTimeEntity {
private Long id;

@ManyToOne
@JoinColumn(name = "follower_id")
private Member follower;
@JoinColumn(name = "source_id")
private Member source;

@ManyToOne
@JoinColumn(name = "following_id")
private Member following;
@JoinColumn(name = "target_id")
private Member target;

@Builder(access = AccessLevel.PRIVATE)
private MemberRelation(Long id, Member follower, Member following) {
private MemberRelation(Long id, Member source, Member target) {
this.id = id;
this.follower = follower;
this.following = following;
this.source = source;
this.target = target;
}

public static MemberRelation createMemberRelation(Member follower, Member following) {
return MemberRelation.builder().follower(follower).following(following).build();
public static MemberRelation createMemberRelation(Member source, Member target) {
return MemberRelation.builder().source(source).target(target).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.depromeet.domain.follow.dto.response;

public record FollowFindMeInfoResponse(Long followingCount, Long followerCount) {
public static FollowFindMeInfoResponse of(Long followingCount, Long followerCount) {
return new FollowFindMeInfoResponse(followingCount, followerCount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.depromeet.domain.follow.dto.response;

public record FollowFindTargetInfoResponse(
Long followingCount, Long followerCount, FollowStatus followStatus) {
public static FollowFindTargetInfoResponse of(
Long followingCount, Long followerCount, FollowStatus followStatus) {
return new FollowFindTargetInfoResponse(followingCount, followerCount, followStatus);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.depromeet.domain.follow.dto.response;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum FollowStatus {
FOLLOWING("팔로잉"), // A가 이미 B를 팔로우 중일때 (팔로우 취소)
FOLLOWED_BY_ME("맞팔로우"), // B가 A를 팔로우 중일때 (맞팔로우)
NOT_FOLLOWING("팔로우"), // A가 B를 팔로우하지 않고, B도 A를 팔로우하지 않을 때 (팔로우 추가)
;

private final String value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public ResponseEntity<GlobalResponse> handleConstraintViolationException(
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}

/** enum type 일치하지 않아 binding 못할 경우 발생 주로 @RequestParam enum으로 binding 못했을 경우 발생 */
/** PathVariable, RequestParam, RequestHeader, RequestBody 에서 타입이 일치하지 않을 경우 발생 */
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
protected ResponseEntity<GlobalResponse> handleMethodArgumentTypeMismatchException(
MethodArgumentTypeMismatchException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public enum ErrorCode {
SAMPLE_ERROR(HttpStatus.BAD_REQUEST, "Sample Error Message"),

// Common
METHOD_ARGUMENT_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "Enum Type이 일치하지 않아 Binding에 실패하였습니다."),
METHOD_ARGUMENT_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "요청 한 값 타입이 잘못되어 binding에 실패하였습니다."),
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "지원하지 않는 HTTP method 입니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류, 관리자에게 문의하세요"),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.depromeet.domain.follow.application.FollowService;
import com.depromeet.domain.follow.dto.request.FollowCreateRequest;
import com.depromeet.domain.follow.dto.request.FollowDeleteRequest;
import com.depromeet.global.error.exception.ErrorCode;
import com.depromeet.global.security.JwtAuthenticationFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Nested;
Expand Down Expand Up @@ -108,4 +108,53 @@ class 팔로우를_취소할_때 {
.andDo(print());
}
}

@Nested
class 타인의_팔로우_카운트를_확인할_때 {
@Test
void targetId_타입이_일치하지_않은경우_예외가_발생한다() throws Exception {
// given
String targetId = "targetId";

// when, then
mockMvc.perform(get("/follows/{targetId}", targetId))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value()))
.andExpect(
jsonPath("$.data.errorClassName")
.value("MethodArgumentTypeMismatchException"))
.andExpect(
jsonPath("$.data.message")
.value(ErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.getMessage()))
.andDo(print());
}

@Test
void 입력_값이_정상이라면_예외가_발생하지_않는다() throws Exception {
// given
Long targetId = 215L;

// when, then
mockMvc.perform(get("/follows/{targetId}", targetId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.status").value(HttpStatus.OK.value()))
.andDo(print());
}
}

@Nested
class 나의_팔로우_카운트를_확인할_때 {

@Test
void 입력_값이_정상이라면_예외가_발생하지_않는다() throws Exception {
// when, then
mockMvc.perform(get("/follows/me"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.status").value(HttpStatus.OK.value()))
.andDo(print());
}
}
}
Loading

0 comments on commit f88d861

Please sign in to comment.