-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ✨ 사용자가 수신한 푸시 알림 리스트 최신순 조회 (#134)
* test: notifications controller unit test 작성 * feat: notification controller 클래스 생성 * feat: notification use case 클래스 작성 * test: pageable param 전달 테스트 케이스 * test: given 절 반환 dto 수정 * feat: notification info & slice dto 정의 * fix: notification use case 껍데기 구현 * fix: controller use case 호출 * test: notification fixture 상수 및 dto 생성 메서드 추가 * test: 응답 json 경로 수정 * fix: get notifications controller success response로 응답 포맷 수정 * test: controller 응답 포맷 테스트 * fix: get notifications pageable size default 20 -> 30 * feat: notification slide select 메서드 추가 * fix: notification repository jpa_repository -> extended_repository 인터페이스 변경 * fix: controller pagable sort dirction 내림차순 옵션 추가 * feat: notificaiton search service impl * feat: notification use case 내, service 및 mapper 호출 로직 처리 * feat: notification dto info builder 추가 * fix: notification use case import notification mapper * feat: notification mapper 메서드 정의 * feat: notification table 수정 squash merge * fix: formatting 메서드 수정 * refactor: 포매팅된 title, content 로직을 notification 엔티티 메서드로 제공 * test: notification fixture to_entity 주입 방식 수정 및 dummy dto 생성 로직 제거 * refactor: notification info dto 생성 로직 mapper -> dto로 이전 * docs: swagger config에 notification와 storage 태깅 추가 * docs: swagger 문서 추가 * test: domain 모듈 notification service unit test * fix: notification 정기 지출 알림 쿼리 조건문 수정 * rename: 공지 타입 포맷팅 메서드 주석 추가 * rename: notification entity 내 포맷팅 메서드 주석 추가
- Loading branch information
1 parent
127cf5c
commit 869be8d
Showing
18 changed files
with
664 additions
and
55 deletions.
There are no files selected for viewing
63 changes: 63 additions & 0 deletions
63
...-external-api/src/main/java/kr/co/pennyway/api/apis/notification/api/NotificationApi.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package kr.co.pennyway.api.apis.notification.api; | ||
|
||
|
||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.Parameter; | ||
import io.swagger.v3.oas.annotations.Parameters; | ||
import io.swagger.v3.oas.annotations.enums.ParameterIn; | ||
import io.swagger.v3.oas.annotations.media.ArraySchema; | ||
import io.swagger.v3.oas.annotations.media.Content; | ||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import io.swagger.v3.oas.annotations.media.SchemaProperty; | ||
import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import kr.co.pennyway.api.apis.notification.dto.NotificationDto; | ||
import kr.co.pennyway.api.common.security.authentication.SecurityUserDetails; | ||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.data.domain.Sort; | ||
import org.springframework.data.web.PageableDefault; | ||
import org.springframework.data.web.SortDefault; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
|
||
@Tag(name = "[알림 API]") | ||
public interface NotificationApi { | ||
@Operation(summary = "수신한 알림 목록 무한 스크롤 조회") | ||
@Parameters({ | ||
@Parameter( | ||
in = ParameterIn.QUERY, | ||
description = "조회하려는 페이지 (0..N) (기본 값 : 0)", | ||
name = "page", | ||
example = "0", | ||
schema = @Schema( | ||
type = "integer", | ||
defaultValue = "0" | ||
) | ||
), | ||
@Parameter( | ||
in = ParameterIn.QUERY, | ||
description = "페이지 내 데이터 수 (기본 값 : 30)", | ||
name = "size", | ||
example = "30", | ||
schema = @Schema( | ||
type = "integer", | ||
defaultValue = "30" | ||
) | ||
), | ||
@Parameter( | ||
in = ParameterIn.QUERY, | ||
description = "정렬 기준 (기본 값 : notification.createdAt,DESC)", | ||
name = "sort", | ||
example = "notification.createdAt,DESC", | ||
array = @ArraySchema( | ||
schema = @Schema( | ||
type = "string" | ||
) | ||
) | ||
), @Parameter(name = "pageable", hidden = true)}) | ||
@ApiResponse(responseCode = "200", description = "알림 목록 조회 성공", content = @Content(schemaProperties = @SchemaProperty(name = "notifications", schema = @Schema(implementation = NotificationDto.SliceRes.class)))) | ||
ResponseEntity<?> getNotifications( | ||
@PageableDefault(page = 0, size = 30) @SortDefault(sort = "notification.createdAt", direction = Sort.Direction.DESC) Pageable pageable, | ||
@AuthenticationPrincipal SecurityUserDetails user | ||
); | ||
} |
38 changes: 38 additions & 0 deletions
38
...src/main/java/kr/co/pennyway/api/apis/notification/controller/NotificationController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package kr.co.pennyway.api.apis.notification.controller; | ||
|
||
import kr.co.pennyway.api.apis.notification.api.NotificationApi; | ||
import kr.co.pennyway.api.apis.notification.usecase.NotificationUseCase; | ||
import kr.co.pennyway.api.common.response.SuccessResponse; | ||
import kr.co.pennyway.api.common.security.authentication.SecurityUserDetails; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.data.domain.Sort; | ||
import org.springframework.data.web.PageableDefault; | ||
import org.springframework.data.web.SortDefault; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.security.access.prepost.PreAuthorize; | ||
import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
@Slf4j | ||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/v2/notifications") | ||
public class NotificationController implements NotificationApi { | ||
private static final String NOTIFICATIONS = "notifications"; | ||
|
||
private final NotificationUseCase notificationUseCase; | ||
|
||
@Override | ||
@GetMapping("") | ||
@PreAuthorize("isAuthenticated()") | ||
public ResponseEntity<?> getNotifications( | ||
@PageableDefault(page = 0, size = 30) @SortDefault(sort = "notification.createdAt", direction = Sort.Direction.DESC) Pageable pageable, | ||
@AuthenticationPrincipal SecurityUserDetails user | ||
) { | ||
return ResponseEntity.ok(SuccessResponse.from(NOTIFICATIONS, notificationUseCase.getNotifications(user.getUserId(), pageable))); | ||
} | ||
} |
80 changes: 80 additions & 0 deletions
80
...-external-api/src/main/java/kr/co/pennyway/api/apis/notification/dto/NotificationDto.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package kr.co.pennyway.api.apis.notification.dto; | ||
|
||
import com.fasterxml.jackson.annotation.JsonFormat; | ||
import com.fasterxml.jackson.annotation.JsonInclude; | ||
import com.fasterxml.jackson.databind.annotation.JsonSerialize; | ||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; | ||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import kr.co.pennyway.domain.domains.notification.domain.Notification; | ||
import kr.co.pennyway.domain.domains.notification.type.NoticeType; | ||
import lombok.Builder; | ||
import org.springframework.data.domain.Pageable; | ||
|
||
import java.time.LocalDateTime; | ||
import java.util.List; | ||
|
||
public class NotificationDto { | ||
@Schema(title = "푸시 알림 슬라이스 응답") | ||
public record SliceRes( | ||
@Schema(description = "푸시 알림 리스트") | ||
List<Info> content, | ||
@Schema(description = "현재 페이지 번호") | ||
int currentPageNumber, | ||
@Schema(description = "페이지 크기") | ||
int pageSize, | ||
@Schema(description = "전체 요소 개수") | ||
int numberOfElements, | ||
@Schema(description = "다음 페이지 존재 여부") | ||
boolean hasNext | ||
) { | ||
public static SliceRes from(List<Info> notifications, Pageable pageable, int numberOfElements, boolean hasNext) { | ||
return new SliceRes(notifications, pageable.getPageNumber(), pageable.getPageSize(), numberOfElements, hasNext); | ||
} | ||
} | ||
|
||
@Builder | ||
@Schema(title = "푸시 알림 상세 정보", description = "푸시 알림 pk, 읽음 여부, 제목, 내용, 타입 그리고 딥 링크 정보를 담고 있다.") | ||
public record Info( | ||
@Schema(description = "푸시 알림 pk", example = "1") | ||
Long id, | ||
@Schema(description = "푸시 알림 읽음 여부", example = "true") | ||
boolean isRead, | ||
@Schema(description = "푸시 알림 제목", example = "페니웨이 공지") | ||
String title, | ||
@Schema(description = "푸시 알림 내용", example = "안녕하세요. 페니웨이입니다.") | ||
String content, | ||
@Schema(description = "푸시 알림 타입. ex) ANNOUNCEMENT", example = "FEED_LIKE_FROM_TO") | ||
String type, | ||
@Schema(description = "푸시 알림 행위자. ex) 다른 사용자 <type이 ANNOUNCEMENT면 존재하지 않음>", example = "pennyway") | ||
@JsonInclude(JsonInclude.Include.NON_NULL) | ||
String from, | ||
@Schema(description = "푸시 알림 행위자 pk <type이 ANNOUNCEMENT면 존재하지 않음>", example = "1") | ||
@JsonInclude(JsonInclude.Include.NON_NULL) | ||
Long fromId, | ||
@Schema(description = "푸시 알림 행위자가 액션을 취한 대상 pk. ex) 피드 pk, 댓글 pk <type이 ANNOUNCEMENT면 존재하지 않음>", example = "3") | ||
@JsonInclude(JsonInclude.Include.NON_NULL) | ||
Long toId, | ||
@Schema(description = "푸시 알림 생성 시간", example = "2024-07-17 12:00:00") | ||
@JsonSerialize(using = LocalDateTimeSerializer.class) | ||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||
LocalDateTime createdAt | ||
) { | ||
public static NotificationDto.Info from(Notification notification) { | ||
NotificationDto.Info.InfoBuilder builder = NotificationDto.Info.builder() | ||
.id(notification.getId()) | ||
.isRead(notification.getReadAt() != null) | ||
.title(notification.createFormattedTitle()) | ||
.content(notification.createFormattedContent()) | ||
.type(notification.getType().name()) | ||
.createdAt(notification.getCreatedAt()); | ||
|
||
if (!notification.getType().equals(NoticeType.ANNOUNCEMENT)) { | ||
builder.from(notification.getSenderName()) | ||
.fromId(notification.getSender().getId()) | ||
.toId(notification.getToId()); | ||
} | ||
|
||
return builder.build(); | ||
} | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
...nal-api/src/main/java/kr/co/pennyway/api/apis/notification/mapper/NotificationMapper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package kr.co.pennyway.api.apis.notification.mapper; | ||
|
||
import kr.co.pennyway.api.apis.notification.dto.NotificationDto; | ||
import kr.co.pennyway.common.annotation.Mapper; | ||
import kr.co.pennyway.domain.domains.notification.domain.Notification; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.data.domain.Slice; | ||
|
||
@Slf4j | ||
@Mapper | ||
public class NotificationMapper { | ||
/** | ||
* Slice<Notification> 타입을 무한 스크롤 응답 형태로 변환한다. | ||
*/ | ||
public static NotificationDto.SliceRes toSliceRes(Slice<Notification> notifications, Pageable pageable) { | ||
return NotificationDto.SliceRes.from( | ||
notifications.getContent().stream().map(NotificationDto.Info::from).toList(), | ||
pageable, | ||
notifications.getNumberOfElements(), | ||
notifications.hasNext() | ||
); | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
...src/main/java/kr/co/pennyway/api/apis/notification/service/NotificationSearchService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package kr.co.pennyway.api.apis.notification.service; | ||
|
||
import kr.co.pennyway.domain.domains.notification.domain.Notification; | ||
import kr.co.pennyway.domain.domains.notification.service.NotificationService; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.data.domain.Slice; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@Slf4j | ||
@Service | ||
@RequiredArgsConstructor | ||
public class NotificationSearchService { | ||
private final NotificationService notificationService; | ||
|
||
@Transactional(readOnly = true) | ||
public Slice<Notification> getNotifications(Long userId, Pageable pageable) { | ||
return notificationService.readNotificationsSlice(userId, pageable); | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
...l-api/src/main/java/kr/co/pennyway/api/apis/notification/usecase/NotificationUseCase.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package kr.co.pennyway.api.apis.notification.usecase; | ||
|
||
import kr.co.pennyway.api.apis.notification.dto.NotificationDto; | ||
import kr.co.pennyway.api.apis.notification.mapper.NotificationMapper; | ||
import kr.co.pennyway.api.apis.notification.service.NotificationSearchService; | ||
import kr.co.pennyway.common.annotation.UseCase; | ||
import kr.co.pennyway.domain.domains.notification.domain.Notification; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.data.domain.Slice; | ||
|
||
@Slf4j | ||
@UseCase | ||
@RequiredArgsConstructor | ||
public class NotificationUseCase { | ||
private final NotificationSearchService notificationSearchService; | ||
|
||
public NotificationDto.SliceRes getNotifications(Long userId, Pageable pageable) { | ||
Slice<Notification> notifications = notificationSearchService.getNotifications(userId, pageable); | ||
|
||
return NotificationMapper.toSliceRes(notifications, pageable); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
...a/kr/co/pennyway/api/apis/notification/controller/GetNotificationsControllerUnitTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package kr.co.pennyway.api.apis.notification.controller; | ||
|
||
import kr.co.pennyway.api.apis.notification.dto.NotificationDto; | ||
import kr.co.pennyway.api.apis.notification.usecase.NotificationUseCase; | ||
import kr.co.pennyway.api.config.WebConfig; | ||
import kr.co.pennyway.api.config.fixture.NotificationFixture; | ||
import kr.co.pennyway.api.config.fixture.UserFixture; | ||
import kr.co.pennyway.api.config.supporter.WithSecurityMockUser; | ||
import kr.co.pennyway.domain.domains.notification.domain.Notification; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
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.context.annotation.ComponentScan; | ||
import org.springframework.context.annotation.FilterType; | ||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.test.context.ActiveProfiles; | ||
import org.springframework.test.web.servlet.MockMvc; | ||
import org.springframework.test.web.servlet.ResultActions; | ||
import org.springframework.test.web.servlet.setup.MockMvcBuilders; | ||
import org.springframework.web.context.WebApplicationContext; | ||
|
||
import java.util.List; | ||
|
||
import static org.mockito.ArgumentMatchers.any; | ||
import static org.mockito.ArgumentMatchers.eq; | ||
import static org.mockito.BDDMockito.given; | ||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; | ||
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; | ||
|
||
@WebMvcTest(controllers = {NotificationController.class}, excludeFilters = { | ||
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebConfig.class)}) | ||
@ActiveProfiles("test") | ||
public class GetNotificationsControllerUnitTest { | ||
@Autowired | ||
private MockMvc mockMvc; | ||
@MockBean | ||
private NotificationUseCase notificationUseCase; | ||
|
||
@BeforeEach | ||
void setUp(WebApplicationContext context) { | ||
this.mockMvc = MockMvcBuilders | ||
.webAppContextSetup(context) | ||
.defaultRequest(post("/**").with(csrf())) | ||
.defaultRequest(put("/**").with(csrf())) | ||
.defaultRequest(delete("/**").with(csrf())) | ||
.build(); | ||
} | ||
|
||
@Test | ||
@WithSecurityMockUser | ||
@DisplayName("쿼리 파라미터로 page 외의 파라미터는 기본값을 갖는다.") | ||
void getNotificationsWithDefaultParameters() throws Exception { | ||
// when | ||
int page = 0, currentPageNumber = 0, pageSize = 20, numberOfElements = 1; | ||
Pageable pa = Pageable.ofSize(pageSize).withPage(currentPageNumber); | ||
|
||
Notification notification = NotificationFixture.ANNOUNCEMENT_DAILY_SPENDING.toEntity(UserFixture.GENERAL_USER.toUser()); | ||
NotificationDto.Info info = NotificationDto.Info.from(notification); | ||
|
||
given(notificationUseCase.getNotifications(eq(1L), any())).willReturn(NotificationDto.SliceRes.from(List.of(info), pa, numberOfElements, false)); | ||
|
||
// when | ||
ResultActions result = performGetNotifications(page); | ||
|
||
// then | ||
result.andDo(print()) | ||
.andExpect(status().isOk()) | ||
.andExpect(jsonPath("$.data.notifications.currentPageNumber").value(page)); | ||
} | ||
|
||
@Test | ||
@WithSecurityMockUser | ||
@DisplayName("응답은 무한 스크롤 방식으로 제공되며, content, currentPageNumber, pageSize, numberOfElements, hasNext 필드를 포함한다.") | ||
void getNotificationsWithInfiniteScroll() throws Exception { | ||
// when | ||
int page = 0, currentPageNumber = 0, pageSize = 20, numberOfElements = 1; | ||
Pageable pa = Pageable.ofSize(pageSize).withPage(currentPageNumber); | ||
|
||
Notification notification = NotificationFixture.ANNOUNCEMENT_DAILY_SPENDING.toEntity(UserFixture.GENERAL_USER.toUser()); | ||
NotificationDto.Info info = NotificationDto.Info.from(notification); | ||
NotificationDto.SliceRes sliceRes = NotificationDto.SliceRes.from(List.of(info), pa, numberOfElements, false); | ||
|
||
given(notificationUseCase.getNotifications(eq(1L), any())).willReturn(sliceRes); | ||
|
||
// when | ||
ResultActions result = performGetNotifications(page); | ||
|
||
// then | ||
result.andDo(print()) | ||
.andExpect(status().isOk()) | ||
.andExpect(jsonPath("$.data.notifications.content").exists()) | ||
.andExpect(jsonPath("$.data.notifications.currentPageNumber").value(sliceRes.currentPageNumber())) | ||
.andExpect(jsonPath("$.data.notifications.pageSize").value(sliceRes.pageSize())) | ||
.andExpect(jsonPath("$.data.notifications.numberOfElements").value(sliceRes.numberOfElements())) | ||
.andExpect(jsonPath("$.data.notifications.hasNext").value(sliceRes.hasNext())); | ||
} | ||
|
||
private ResultActions performGetNotifications(int page) throws Exception { | ||
return mockMvc.perform(get("/v2/notifications") | ||
.param("page", String.valueOf(page))); | ||
} | ||
} |
Oops, something went wrong.