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

BE/#224 팔로우 시, 팔로우한 사용자에게 알림 전송 기능 구현 #233

Merged
merged 9 commits into from
Oct 1, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,63 @@
import com.graphy.backend.domain.member.domain.Member;
import com.graphy.backend.domain.member.dto.response.GetMemberListResponse;
import com.graphy.backend.domain.member.repository.MemberRepository;
import com.graphy.backend.domain.member.service.MemberService;
import com.graphy.backend.domain.notification.domain.NotificationType;
import com.graphy.backend.domain.notification.dto.NotificationDto;
import com.graphy.backend.domain.notification.service.NotificationService;
import com.graphy.backend.global.error.ErrorCode;
import com.graphy.backend.global.error.exception.AlreadyExistException;
import com.graphy.backend.global.error.exception.EmptyResultException;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.transaction.Transactional;
import java.util.List;


@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
@Service
@Transactional
@Transactional(readOnly = true)
public class FollowService {
private final FollowRepository followRepository;
private final MemberRepository memberRepository;

private final NotificationService notificationService;
private final MemberService memberService;


@Transactional
public void addFollow(Long toId, Member loginUser) {
memberService.findMemberById(loginUser.getId());

Long fromId = loginUser.getId();
checkFollowingAlready(loginUser.getId(), toId);
checkFollowAvailable(loginUser.getId(), toId);

Follow follow = Follow.builder().fromId(fromId).toId(toId).build();
NotificationType notificationType = NotificationType.FOLLOW;
notificationType.setMessage(loginUser.getNickname(), "");
NotificationDto notificationDto = NotificationDto.builder()
.type(notificationType)
.content(notificationType.getMessage())
.build();

followRepository.save(follow);
memberRepository.increaseFollowerCount(toId);
memberRepository.increaseFollowingCount(fromId);

notificationService.addNotification(notificationDto, toId);
}

@Transactional
public void removeFollow(Long toId, Member loginUser) {
Long fromId = loginUser.getId();
Follow follow = followRepository.findByFromIdAndToId(fromId, toId).orElseThrow(
() -> new EmptyResultException(ErrorCode.FOLLOW_NOT_EXIST)
);
followRepository.delete(follow);

// TODO: memberService의 메소드로 분리
memberRepository.decreaseFollowerCount(toId);
memberRepository.decreaseFollowingCount(fromId);
}
Expand All @@ -50,9 +74,12 @@ public List<GetMemberListResponse> findFollowingList(Member loginUser) {
return followRepository.findFollowings(loginUser.getId());
}

public void checkFollowingAlready(Long fromId, Long toId) {
public void checkFollowAvailable(Long fromId, Long toId) {
if (followRepository.existsByFromIdAndToId(fromId, toId)) {
throw new AlreadyExistException(ErrorCode.FOLLOW_ALREADY_EXIST);
}
if (fromId.equals(toId)) {
throw new AlreadyExistException(ErrorCode.FOLLOW_SELF);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.graphy.backend.domain.notification.domain;

import com.graphy.backend.domain.member.domain.Member;
import com.graphy.backend.global.common.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.SQLDelete;

import javax.persistence.*;

@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@SQLDelete(sql = "UPDATE notification SET is_deleted = true WHERE notification_id = ?")
public class Notification extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "notification_id")
private Long id;

@ManyToOne
@JoinColumn(name = "member_id", nullable = false)
private Member member;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private NotificationType type;

@Column(nullable = false)
private String content;

@Column(nullable = false)
private boolean isRead;

@Column(nullable = false)
private boolean isEmailSent;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.graphy.backend.domain.notification.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum NotificationType {
RECRUITMENT("님이 팀 합류를 "), // 팀원 모집 글 관련 알림 (지원 신청, 지원 신청 수락)
FOLLOW("님이 팔로우하였습니다."), // 팔로우 관련 알림(본인을 팔로우하는 사용자가 발생 시)
MESSAGE("님이 쪽지를 보냈습니다."); // 쪽지 알림 (쪽지 수신 시)

private String message;

public void setMessage(String username, String extraMessage) {
this.message = username + message +extraMessage;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.graphy.backend.domain.notification.dto;

import com.graphy.backend.domain.member.domain.Member;
import com.graphy.backend.domain.notification.domain.Notification;
import com.graphy.backend.domain.notification.domain.NotificationType;
import lombok.*;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class NotificationDto {
private NotificationType type;
private Member member;
private String content;
private boolean isRead = false;
private boolean isEmailSent = false;

public void setMember(Member member) {
this.member = member;
}

public Notification toEntity() {
return Notification.builder()
.type(type)
.member(member)
.content(content)
.isRead(this.isRead)
.isEmailSent(this.isEmailSent)
.build();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.graphy.backend.domain.notification.repository;

import com.graphy.backend.domain.notification.domain.Notification;
import org.springframework.data.jpa.repository.JpaRepository;

public interface NotificationRepository extends JpaRepository<Notification, Long> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.graphy.backend.domain.notification.service;

import com.graphy.backend.domain.member.domain.Member;
import com.graphy.backend.domain.member.service.MemberService;
import com.graphy.backend.domain.notification.dto.NotificationDto;
import com.graphy.backend.domain.notification.repository.NotificationRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
@Transactional(readOnly = true)
public class NotificationService {
private final NotificationRepository notificationRepository;
private final MemberService memberService;

@Transactional
public void addNotification(NotificationDto dto, Long memberId) {
Member member = memberService.findMemberById(memberId);
dto.setMember(member);

notificationRepository.save(dto.toEntity());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ public enum ErrorCode {

// Follow
FOLLOW_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "F001", "이미 존재하는 팔로우"),
FOLLOW_NOT_EXIST(HttpStatus.NOT_FOUND, "M002", "존재하지 않는 팔로우"),
FOLLOW_NOT_EXIST(HttpStatus.NOT_FOUND, "F002", "존재하지 않는 팔로우"),
FOLLOW_SELF(HttpStatus.CONFLICT, "F003", "자기 자신을 팔로우 할 수 없음"),

// Project
PROJECT_DELETED_OR_NOT_EXIST(HttpStatus.NOT_FOUND, "P001", "이미 삭제되거나 존재하지 않는 프로젝트"),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.graphy.backend.domain.member.domain.Member;
import com.graphy.backend.domain.member.dto.response.GetMemberListResponse;
import com.graphy.backend.domain.member.repository.MemberRepository;
import com.graphy.backend.domain.member.service.MemberService;
import com.graphy.backend.domain.notification.service.NotificationService;
import com.graphy.backend.global.error.exception.AlreadyExistException;
import com.graphy.backend.global.error.exception.EmptyResultException;
import com.graphy.backend.test.MockTest;
Expand All @@ -21,8 +23,7 @@
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
Expand All @@ -31,19 +32,28 @@ class FollowServiceTest extends MockTest {
FollowRepository followRepository;
@Mock
MemberRepository memberRepository;

@Mock
NotificationService notificationService;
@Mock
MemberService memberService;

@InjectMocks
FollowService followService;

@Test
@DisplayName("팔로우 신청 테스트")
void followTest() throws Exception {
void followTest() {
//given
Member fromMember = Member.builder().id(1L).build();
Long toId = 2L;

//when
doNothing().when(memberRepository).increaseFollowingCount(fromMember.getId());
doNothing().when(memberRepository).increaseFollowerCount(toId);
doNothing().when(notificationService).addNotification(any(), any());
when(memberService.findMemberById(fromMember.getId())).thenReturn(fromMember);

followService.addFollow(toId, fromMember);

//then
Expand All @@ -52,7 +62,7 @@ void followTest() throws Exception {

@Test
@DisplayName("팔로잉 리스트 조회 테스트")
void getFollowingListTest() throws Exception {
void getFollowingListTest() {
//given
Member fromMember = Member.builder().id(1L).build();
GetMemberListResponse following1 = new GetMemberListResponse() {
Expand Down Expand Up @@ -87,7 +97,7 @@ public String getNickname() {

@Test
@DisplayName("팔로워 리스트 조회 테스트")
void getFollowerListTest() throws Exception {
void getFollowerListTest() {
//given
Member toMember = Member.builder().id(1L).build();
GetMemberListResponse follower1 = new GetMemberListResponse() {
Expand Down Expand Up @@ -122,7 +132,7 @@ public String getNickname() {

@Test
@DisplayName("언팔로우 테스트")
void unfollowTest() throws Exception {
void unfollowTest() {
//given
Long toId = 1L;
Member fromMember = Member.builder().id(2L).build();
Expand All @@ -149,31 +159,28 @@ void unfollowNotFoundTest() {
.thenReturn(Optional.empty());

// when
Exception exception = assertThrows(EmptyResultException.class, () -> {
followService.removeFollow(toId, fromMember);
});
Exception exception = assertThrows(EmptyResultException.class, () -> followService.removeFollow(toId, fromMember));

// then
String exceptionMessage = exception.getMessage();

assertTrue(exceptionMessage.equals("존재하지 않는 팔로우"));
assertEquals("존재하지 않는 팔로우", exceptionMessage);
}

@Test
@DisplayName("팔로우 여부 체크 테스트")
void followingCheckTest() throws Exception {
void followingCheckTest() {
// given
when(followRepository.existsByFromIdAndToId(1L, 2L)).thenReturn(true);
when(followRepository.existsByFromIdAndToId(3L, 4L)).thenReturn(false);

// when & then
Member loginUser = new Member();
assertThatThrownBy(
() -> followService.checkFollowingAlready(1L, 2L))
() -> followService.checkFollowAvailable(1L, 2L))
.isInstanceOf(AlreadyExistException.class)
.hasMessageContaining("이미 존재하는 팔로우");

Assertions.assertThatCode(() -> followService.checkFollowingAlready(3L, 4L))
Assertions.assertThatCode(() -> followService.checkFollowAvailable(3L, 4L))
.doesNotThrowAnyException();
}
}
Loading