Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 팔로우 추가 / 취소 기능 #191

Merged
merged 6 commits into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.depromeet.domain.follow.api;

import com.depromeet.domain.follow.application.FollowService;
import com.depromeet.domain.follow.dto.request.FollowCreateRequest;
import com.depromeet.domain.follow.dto.request.FollowDeleteRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Tag(name = "5. [팔로우]", description = "팔로우 관련 API입니다.")
@RestController
@RequestMapping("/follows")
@RequiredArgsConstructor
public class FollowController {
private final FollowService followService;

@PostMapping
@Operation(summary = "팔로우 추가", description = "팔로우를 추가합니다.")
public ResponseEntity<Void> followCreate(@Valid @RequestBody FollowCreateRequest request) {
followService.createFollow(request);
return ResponseEntity.status(HttpStatus.CREATED).build();
}

@DeleteMapping
@Operation(summary = "팔로우 취소", description = "팔로우를 취소합니다.")
public void followDelete(@Valid @RequestBody FollowDeleteRequest request) {
followService.deleteFollow(request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.depromeet.domain.follow.application;

import com.depromeet.domain.follow.dao.MemberRelationRepository;
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.member.dao.MemberRepository;
import com.depromeet.domain.member.domain.Member;
import com.depromeet.global.error.exception.CustomException;
import com.depromeet.global.error.exception.ErrorCode;
import com.depromeet.global.util.MemberUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class FollowService {
private final MemberUtil memberUtil;
private final MemberRepository memberRepository;
private final MemberRelationRepository memberRelationRepository;

public void createFollow(FollowCreateRequest request) {
final Member currentMember = memberUtil.getCurrentMember();
Member targetMember = getTargetMember(request.targetId());

boolean existMemberRelation =
memberRelationRepository.existsByFollowerIdAndFollowingId(
currentMember.getId(), targetMember.getId());
if (existMemberRelation) {
throw new CustomException(ErrorCode.FOLLOW_ALREADY_EXIST);
}

MemberRelation memberRelation =
MemberRelation.createMemberRelation(currentMember, targetMember);
memberRelationRepository.save(memberRelation);
}

public void deleteFollow(FollowDeleteRequest request) {
final Member currentMember = memberUtil.getCurrentMember();
Member targetMember = getTargetMember(request.targetId());

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

memberRelationRepository.delete(memberRelation);
}

private Member getTargetMember(Long targetId) {
Member targetMember =
memberRepository
.findById(targetId)
.orElseThrow(
() ->
new CustomException(
ErrorCode.FOLLOW_TARGET_MEMBER_NOT_FOUND));
return targetMember;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.depromeet.domain.follow.dao;

import com.depromeet.domain.follow.domain.MemberRelation;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

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

boolean existsByFollowerIdAndFollowingId(Long followerId, Long followingId);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.depromeet.domain.relation.domain;
package com.depromeet.domain.follow.domain;

import com.depromeet.domain.common.model.BaseTimeEntity;
import com.depromeet.domain.member.domain.Member;
Expand All @@ -12,6 +12,7 @@
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

Expand All @@ -37,4 +38,15 @@ public class MemberRelation extends BaseTimeEntity {
@ManyToOne
@JoinColumn(name = "following_id")
private Member following;

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

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

import jakarta.validation.constraints.NotNull;

public record FollowCreateRequest(@NotNull(message = "타겟 아이디는 비워둘 수 없습니다.") Long targetId) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.depromeet.domain.follow.dto.request;

import jakarta.validation.constraints.NotNull;

public record FollowDeleteRequest(@NotNull(message = "타겟 아이디는 비워둘 수 없습니다.") Long targetId) {}
Empty file.

This file was deleted.

Empty file.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ public enum ErrorCode {
MISSION_RECORD_UPLOAD_STATUS_IS_NOT_PENDING(
HttpStatus.BAD_REQUEST, "미션 기록의 이미지 업로드 상태가 PENDING이 아닙니다."),
MISSION_RECORD_ALREADY_EXISTS_TODAY(HttpStatus.BAD_REQUEST, "오늘 이미 작성 된 미션 기록이 존재합니다."),

// Follow
FOLLOW_TARGET_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "타겟 유저을 찾을 수 없습니다."),
FOLLOW_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "이미 팔로우 중인 회원입니다."),
FOLLOW_NOT_EXIST(HttpStatus.BAD_REQUEST, "팔로우 중인 회원만 팔로우 취소가 가능합니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.depromeet.domain.follow.api;

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.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.security.JwtAuthenticationFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Nested;
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.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext;
import org.springframework.http.HttpStatus;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(FollowController.class)
@AutoConfigureMockMvc(addFilters = false)
@MockBean({JpaMetamodelMappingContext.class, JwtAuthenticationFilter.class})
class FollowControllerTest {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
@MockBean private FollowService followService;

@Nested
class 팔로우를_추가할_때 {
@Test
void targetId가_NULL_이라면_예외를_발생시킨다() throws Exception {
// given
FollowCreateRequest request = new FollowCreateRequest(null);

// when, then
mockMvc.perform(
post("/follows")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value()))
.andExpect(
jsonPath("$.data.errorClassName")
.value("MethodArgumentNotValidException"))
.andExpect(jsonPath("$.data.message").value("타겟 아이디는 비워둘 수 없습니다."))
.andDo(print());
}

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

// when, then
mockMvc.perform(
post("/follows")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.status").value(HttpStatus.CREATED.value()))
.andDo(print());
}
}

@Nested
class 팔로우를_취소할_때 {
@Test
void targetId가_NULL_이라면_예외를_발생시킨다() throws Exception {
// given
FollowDeleteRequest request = new FollowDeleteRequest(null);

// when, then
mockMvc.perform(
delete("/follows")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value()))
.andExpect(
jsonPath("$.data.errorClassName")
.value("MethodArgumentNotValidException"))
.andExpect(jsonPath("$.data.message").value("타겟 아이디는 비워둘 수 없습니다."))
.andDo(print());
}

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

// when, then
mockMvc.perform(
delete("/follows")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.status").value(HttpStatus.OK.value()))
.andDo(print());
}
}
}
Loading