diff --git a/backend/src/main/java/com/graphy/backend/domain/follow/service/FollowService.java b/backend/src/main/java/com/graphy/backend/domain/follow/service/FollowService.java index ddd7a86a..2af3a236 100644 --- a/backend/src/main/java/com/graphy/backend/domain/follow/service/FollowService.java +++ b/backend/src/main/java/com/graphy/backend/domain/follow/service/FollowService.java @@ -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); } @@ -50,9 +74,12 @@ public List 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); + } } } diff --git a/backend/src/main/java/com/graphy/backend/domain/notification/domain/Notification.java b/backend/src/main/java/com/graphy/backend/domain/notification/domain/Notification.java new file mode 100644 index 00000000..75ce7fd9 --- /dev/null +++ b/backend/src/main/java/com/graphy/backend/domain/notification/domain/Notification.java @@ -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; +} diff --git a/backend/src/main/java/com/graphy/backend/domain/notification/domain/NotificationType.java b/backend/src/main/java/com/graphy/backend/domain/notification/domain/NotificationType.java new file mode 100644 index 00000000..bda4a2b8 --- /dev/null +++ b/backend/src/main/java/com/graphy/backend/domain/notification/domain/NotificationType.java @@ -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; + } +} diff --git a/backend/src/main/java/com/graphy/backend/domain/notification/dto/NotificationDto.java b/backend/src/main/java/com/graphy/backend/domain/notification/dto/NotificationDto.java new file mode 100644 index 00000000..f85a450b --- /dev/null +++ b/backend/src/main/java/com/graphy/backend/domain/notification/dto/NotificationDto.java @@ -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(); + } +} + diff --git a/backend/src/main/java/com/graphy/backend/domain/notification/repository/NotificationRepository.java b/backend/src/main/java/com/graphy/backend/domain/notification/repository/NotificationRepository.java new file mode 100644 index 00000000..72c5f814 --- /dev/null +++ b/backend/src/main/java/com/graphy/backend/domain/notification/repository/NotificationRepository.java @@ -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 { + +} diff --git a/backend/src/main/java/com/graphy/backend/domain/notification/service/NotificationService.java b/backend/src/main/java/com/graphy/backend/domain/notification/service/NotificationService.java new file mode 100644 index 00000000..a23c2ec9 --- /dev/null +++ b/backend/src/main/java/com/graphy/backend/domain/notification/service/NotificationService.java @@ -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()); + } +} diff --git a/backend/src/main/java/com/graphy/backend/global/error/ErrorCode.java b/backend/src/main/java/com/graphy/backend/global/error/ErrorCode.java index fe077517..f006b061 100644 --- a/backend/src/main/java/com/graphy/backend/global/error/ErrorCode.java +++ b/backend/src/main/java/com/graphy/backend/global/error/ErrorCode.java @@ -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", "이미 삭제되거나 존재하지 않는 프로젝트"), diff --git a/backend/src/test/java/com/graphy/backend/domain/follow/service/FollowServiceTest.java b/backend/src/test/java/com/graphy/backend/domain/follow/service/FollowServiceTest.java index 8d8805b6..633e5ed4 100644 --- a/backend/src/test/java/com/graphy/backend/domain/follow/service/FollowServiceTest.java +++ b/backend/src/test/java/com/graphy/backend/domain/follow/service/FollowServiceTest.java @@ -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; @@ -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) @@ -31,12 +32,18 @@ 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; @@ -44,6 +51,9 @@ void followTest() throws Exception { //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 @@ -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() { @@ -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() { @@ -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(); @@ -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(); } }