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] 회원 알림 목록 조회 #194

Merged
merged 4 commits into from
Feb 6, 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
4 changes: 4 additions & 0 deletions src/main/java/codesquad/fineants/domain/BaseEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package codesquad.fineants.domain.notification;

import java.time.LocalDateTime;

import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

import codesquad.fineants.domain.BaseEntity;
import codesquad.fineants.domain.member.Member;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Notification extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String title;
private String content;
private Boolean isRead;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;

@Builder
private Notification(LocalDateTime createAt, LocalDateTime modifiedAt, Long id, String title,
String content, Boolean isRead, Member member) {
super(createAt, modifiedAt);
this.id = id;
this.title = title;
this.content = content;
this.isRead = isRead;
this.member = member;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package codesquad.fineants.domain.notification;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface NotificationRepository extends JpaRepository<Notification, Long> {

@Query("select n from Notification n where n.member.id = :memberId order by n.createAt desc")
List<Notification> findAllByMemberId(@Param("memberId") Long memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package codesquad.fineants.spring.api.member.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import codesquad.fineants.spring.api.member.response.MemberNotificationResponse;
import codesquad.fineants.spring.api.member.service.MemberNotificationService;
import codesquad.fineants.spring.api.response.ApiResponse;
import codesquad.fineants.spring.api.success.code.MemberSuccessCode;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api/members/{memberId}")
@RequiredArgsConstructor
public class MemberNotificationRestController {

private final MemberNotificationService notificationService;

@GetMapping("/notifications")
public ApiResponse<MemberNotificationResponse> readNotifications(@PathVariable Long memberId) {
return ApiResponse.success(MemberSuccessCode.OK_READ_NOTIFICATIONS,
notificationService.readNotifications(memberId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package codesquad.fineants.spring.api.member.response;

import java.time.LocalDateTime;

import codesquad.fineants.domain.notification.Notification;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode(of = "notificationId")
@ToString
@Builder
public class MemberNotification {
private Long notificationId;
private String title;
private String content;
private LocalDateTime timestamp;
private Boolean isRead;

public static MemberNotification from(Notification notification) {
return MemberNotification.builder()
.notificationId(notification.getId())
.title(notification.getTitle())
.content(notification.getContent())
.timestamp(notification.getCreateAt())
.isRead(notification.getIsRead())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package codesquad.fineants.spring.api.member.response;

import java.util.List;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
public class MemberNotificationResponse {
private List<MemberNotification> notifications;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package codesquad.fineants.spring.api.member.service;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import codesquad.fineants.domain.notification.Notification;
import codesquad.fineants.domain.notification.NotificationRepository;
import codesquad.fineants.spring.api.member.response.MemberNotification;
import codesquad.fineants.spring.api.member.response.MemberNotificationResponse;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberNotificationService {

private final NotificationRepository notificationRepository;

public MemberNotificationResponse readNotifications(Long memberId) {
List<Notification> notifications = notificationRepository.findAllByMemberId(memberId);
return new MemberNotificationResponse(
notifications.stream()
.map(MemberNotification::from)
.collect(Collectors.toList())
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ public enum MemberSuccessCode implements SuccessCode {
OK_PASSWORD_CHANGED(HttpStatus.OK, "비밀번호를 성공적으로 변경했습니다."),
OK_VERIF_CODE(HttpStatus.OK, "일치하는 인증번호 입니다"),
OK_DELETED_ACCOUNT(HttpStatus.OK, "계정이 삭제되었습니다"),
OK_LOGIN(HttpStatus.OK, "로그인에 성공하였습니다.");
OK_LOGIN(HttpStatus.OK, "로그인에 성공하였습니다."),
OK_READ_NOTIFICATIONS(HttpStatus.OK, "현재 알림 목록 조회를 성공했습니다");

private final HttpStatus httpStatus;
private final String message;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package codesquad.fineants.spring.api.member.controller;

import static org.hamcrest.Matchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.anyLong;
import static org.mockito.BDDMockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import java.time.LocalDateTime;
import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.core.MethodParameter;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import codesquad.fineants.domain.member.Member;
import codesquad.fineants.domain.oauth.support.AuthMember;
import codesquad.fineants.domain.oauth.support.AuthPrincipalArgumentResolver;
import codesquad.fineants.spring.api.errors.handler.GlobalExceptionHandler;
import codesquad.fineants.spring.api.member.response.MemberNotification;
import codesquad.fineants.spring.api.member.response.MemberNotificationResponse;
import codesquad.fineants.spring.api.member.service.MemberNotificationService;
import codesquad.fineants.spring.config.JpaAuditingConfiguration;

@ActiveProfiles("test")
@WebMvcTest(controllers = MemberNotificationRestController.class)
@MockBean(JpaAuditingConfiguration.class)
class MemberNotificationRestControllerTest {

private MockMvc mockMvc;

@Autowired
private MemberNotificationRestController memberNotificationRestController;

@Autowired
private GlobalExceptionHandler globalExceptionHandler;

@MockBean
private AuthPrincipalArgumentResolver authPrincipalArgumentResolver;

@MockBean
private MemberNotificationService notificationService;

@BeforeEach
void setup() {
mockMvc = MockMvcBuilders.standaloneSetup(memberNotificationRestController)
.setControllerAdvice(globalExceptionHandler)
.setCustomArgumentResolvers(authPrincipalArgumentResolver)
.alwaysDo(print())
.build();

given(authPrincipalArgumentResolver.supportsParameter(ArgumentMatchers.any(MethodParameter.class)))
.willReturn(true);
given(authPrincipalArgumentResolver.resolveArgument(any(), any(), any(), any()))
.willReturn(AuthMember.from(createMember()));
}

@DisplayName("사용자는 알림 목록 조회합니다")
@Test
void readNotifications() throws Exception {
// given
Member member = createMember();

List<MemberNotification> mockNotifications = createNotifications();
given(notificationService.readNotifications(anyLong()))
.willReturn(new MemberNotificationResponse(mockNotifications));

// when & then
mockMvc.perform(get("/api/members/{memberId}/notifications", member.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("code").value(equalTo(200)))
.andExpect(jsonPath("status").value(equalTo("OK")))
.andExpect(jsonPath("message").value(equalTo("현재 알림 목록 조회를 성공했습니다")))
.andExpect(jsonPath("data.notifications").isArray())
.andExpect(jsonPath("data.notifications[0].notificationId").value(equalTo(3)))
.andExpect(jsonPath("data.notifications[1].notificationId").value(equalTo(2)))
.andExpect(jsonPath("data.notifications[2].notificationId").value(equalTo(1)));
}

private Member createMember() {
return Member.builder()
.id(1L)
.nickname("일개미1234")
.email("dragonbead95@naver.com")
.provider("local")
.password("password")
.profileUrl("profileUrl")
.build();
}

private List<MemberNotification> createNotifications() {
return List.of(MemberNotification.builder()
.notificationId(3L)
.title("포트폴리오")
.content("포트폴리오2의 최대 손실율을 초과했습니다")
.timestamp(LocalDateTime.of(2024, 1, 24, 10, 10, 10))
.isRead(false)
.build(),
MemberNotification.builder()
.notificationId(2L)
.title("포트폴리오")
.content("포트폴리오1의 목표 수익률을 달성했습니다")
.timestamp(LocalDateTime.of(2024, 1, 23, 10, 10, 10))
.isRead(false)
.build(),
MemberNotification.builder()
.notificationId(1L)
.title("지정가")
.content("삼성전자가 지정가 KRW60000에 도달했습니다")
.timestamp(LocalDateTime.of(2024, 1, 22, 10, 10, 10))
.isRead(true)
.build());
}
}
Loading