diff --git a/keewe-api/src/main/java/ccc/keeweapi/controller/api/notification/NotificationController.java b/keewe-api/src/main/java/ccc/keeweapi/controller/api/notification/NotificationController.java new file mode 100644 index 00000000..b0bd4e03 --- /dev/null +++ b/keewe-api/src/main/java/ccc/keeweapi/controller/api/notification/NotificationController.java @@ -0,0 +1,46 @@ +package ccc.keeweapi.controller.api.notification; + +import static ccc.keewecore.consts.KeeweConsts.LONG_MAX_STRING; + +import ccc.keeweapi.dto.ApiResponse; +import ccc.keeweapi.dto.notification.NotificationResponse; +import ccc.keeweapi.dto.notification.PaginateNotificationResponse; +import ccc.keeweapi.service.notification.command.NotificationCommandApiService; +import ccc.keeweapi.service.notification.query.NotificationQueryApiService; +import ccc.keewedomain.persistence.domain.notification.enums.NotificationCategory; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/notification") +@RequiredArgsConstructor +public class NotificationController { + private final NotificationQueryApiService notificationQueryApiService; + private final NotificationCommandApiService notificationCommandApiService; + + @GetMapping + public ApiResponse paginateNotifications( + @RequestParam(required = false, defaultValue = LONG_MAX_STRING) Long cursor, + @RequestParam Long limit + ) { + //return ApiResponse.ok(notificationQueryApiService.paginateNotifications(CursorPageable.of(cursor, limit))); + return ApiResponse.ok( + PaginateNotificationResponse.of( + 10L, List.of( + NotificationResponse.of(3L, "내 인사이트에 \n누군가 댓글 남김", "유승훈님이 댓글을 남겼어요.", NotificationCategory.COMMENT, "3", false), + NotificationResponse.of(4L, "초보 기록가", "꾸준함이 중요하죠. 초보 기록가!", NotificationCategory.TITLE, "6", true) + )) + ); + } + + @PatchMapping("/{notificationId}/read") + public ApiResponse markAsReadToNotification(@PathVariable("notificationId") Long notificationId) { + return ApiResponse.ok(notificationCommandApiService.markAsRead(notificationId)); + } +} diff --git a/keewe-api/src/main/java/ccc/keeweapi/dto/notification/NotificationResponse.java b/keewe-api/src/main/java/ccc/keeweapi/dto/notification/NotificationResponse.java new file mode 100644 index 00000000..311f0927 --- /dev/null +++ b/keewe-api/src/main/java/ccc/keeweapi/dto/notification/NotificationResponse.java @@ -0,0 +1,16 @@ +package ccc.keeweapi.dto.notification; + +import ccc.keewedomain.persistence.domain.notification.enums.NotificationCategory; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(staticName = "of") +public class NotificationResponse { + private Long id; + private String title; + private String contents; + private NotificationCategory category; + private String referenceId; + private Boolean read; +} diff --git a/keewe-api/src/main/java/ccc/keeweapi/dto/notification/PaginateNotificationResponse.java b/keewe-api/src/main/java/ccc/keeweapi/dto/notification/PaginateNotificationResponse.java new file mode 100644 index 00000000..6d2f5441 --- /dev/null +++ b/keewe-api/src/main/java/ccc/keeweapi/dto/notification/PaginateNotificationResponse.java @@ -0,0 +1,12 @@ +package ccc.keeweapi.dto.notification; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(staticName = "of") +public class PaginateNotificationResponse { + private Long nextCursor; + private List notifications; +} diff --git a/keewe-api/src/main/java/ccc/keeweapi/service/notification/CommentNotificationProcessor.java b/keewe-api/src/main/java/ccc/keeweapi/service/notification/CommentNotificationProcessor.java new file mode 100644 index 00000000..e520b59e --- /dev/null +++ b/keewe-api/src/main/java/ccc/keeweapi/service/notification/CommentNotificationProcessor.java @@ -0,0 +1,36 @@ +package ccc.keeweapi.service.notification; + +import ccc.keeweapi.dto.notification.NotificationResponse; +import ccc.keewedomain.persistence.domain.insight.Comment; +import ccc.keewedomain.persistence.domain.notification.Notification; +import ccc.keewedomain.persistence.domain.notification.enums.NotificationCategory; +import ccc.keewedomain.persistence.domain.notification.enums.NotificationContents; +import ccc.keewedomain.service.insight.CommentDomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class CommentNotificationProcessor implements NotificationProcessor { + private final CommentDomainService commentDomainService; + + @Override + public NotificationCategory getCategory() { + return NotificationCategory.COMMENT; + } + + @Override + public NotificationResponse process(Notification notification) { + String commentId = notification.getReferenceId(); + Comment comment = commentDomainService.getByIdOrElseThrow(Long.parseLong(commentId)); + NotificationContents contents = notification.getContents(); + return NotificationResponse.of( + notification.getId(), + notification.getContents().getTitle(), + String.format(contents.getContents(), comment.getWriter().getNickname()), // note. {UserName}님이 댓글을 남겼어요. + contents.getCategory(), + notification.getReferenceId(), + notification.isRead() + ); + } +} diff --git a/keewe-api/src/main/java/ccc/keeweapi/service/notification/CommentReplyNotificationProcessor.java b/keewe-api/src/main/java/ccc/keeweapi/service/notification/CommentReplyNotificationProcessor.java new file mode 100644 index 00000000..a42212e4 --- /dev/null +++ b/keewe-api/src/main/java/ccc/keeweapi/service/notification/CommentReplyNotificationProcessor.java @@ -0,0 +1,36 @@ +package ccc.keeweapi.service.notification; + +import ccc.keeweapi.dto.notification.NotificationResponse; +import ccc.keewedomain.persistence.domain.insight.Comment; +import ccc.keewedomain.persistence.domain.notification.Notification; +import ccc.keewedomain.persistence.domain.notification.enums.NotificationCategory; +import ccc.keewedomain.persistence.domain.notification.enums.NotificationContents; +import ccc.keewedomain.service.insight.CommentDomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class CommentReplyNotificationProcessor implements NotificationProcessor { + private final CommentDomainService commentDomainService; + + @Override + public NotificationCategory getCategory() { + return NotificationCategory.COMMENT_REPLY; + } + + @Override + public NotificationResponse process(Notification notification) { + String commentReplyId = notification.getReferenceId(); + Comment comment = commentDomainService.getByIdOrElseThrow(Long.parseLong(commentReplyId)); + NotificationContents contents = notification.getContents(); + return NotificationResponse.of( + notification.getId(), + notification.getContents().getTitle(), + String.format(contents.getContents(), comment.getWriter().getNickname()), // note. {UserName}님이 답글을 남겼어요. + contents.getCategory(), + notification.getReferenceId(), + notification.isRead() + ); + } +} diff --git a/keewe-api/src/main/java/ccc/keeweapi/service/notification/NotificationProcessor.java b/keewe-api/src/main/java/ccc/keeweapi/service/notification/NotificationProcessor.java new file mode 100644 index 00000000..6ead69b1 --- /dev/null +++ b/keewe-api/src/main/java/ccc/keeweapi/service/notification/NotificationProcessor.java @@ -0,0 +1,11 @@ +package ccc.keeweapi.service.notification; + + +import ccc.keeweapi.dto.notification.NotificationResponse; +import ccc.keewedomain.persistence.domain.notification.Notification; +import ccc.keewedomain.persistence.domain.notification.enums.NotificationCategory; + +public interface NotificationProcessor { + NotificationCategory getCategory(); + NotificationResponse process(Notification notification); +} diff --git a/keewe-api/src/main/java/ccc/keeweapi/service/notification/ReactionNotificationProcessor.java b/keewe-api/src/main/java/ccc/keeweapi/service/notification/ReactionNotificationProcessor.java new file mode 100644 index 00000000..cd4ec5a5 --- /dev/null +++ b/keewe-api/src/main/java/ccc/keeweapi/service/notification/ReactionNotificationProcessor.java @@ -0,0 +1,39 @@ +package ccc.keeweapi.service.notification; + +import ccc.keeweapi.dto.notification.NotificationResponse; +import ccc.keewedomain.persistence.domain.insight.Reaction; +import ccc.keewedomain.persistence.domain.notification.Notification; +import ccc.keewedomain.persistence.domain.notification.enums.NotificationCategory; +import ccc.keewedomain.persistence.domain.notification.enums.NotificationContents; +import ccc.keewedomain.persistence.domain.user.User; +import ccc.keewedomain.service.insight.ReactionDomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class ReactionNotificationProcessor implements NotificationProcessor { + private final ReactionDomainService reactionDomainService; + + @Override + public NotificationCategory getCategory() { + return NotificationCategory.REACTION; + } + + @Override + public NotificationResponse process(Notification notification) { + String reactionId = notification.getReferenceId(); + + Reaction reaction = reactionDomainService.getByIdOrElseThrow(Long.parseLong(reactionId)); + User user = reaction.getReactor(); + NotificationContents contents = notification.getContents(); + return NotificationResponse.of( + notification.getId(), + contents.getTitle(), // note. 내 인사이트에 누군가 반응 남김 + String.format(contents.getContents(), user.getNickname()), // note. {UserName}님이 반응을 남겼어요. + contents.getCategory(), + notification.getReferenceId(), + notification.isRead() + ); + } +} diff --git a/keewe-api/src/main/java/ccc/keeweapi/service/notification/TitleNotificationProcessor.java b/keewe-api/src/main/java/ccc/keeweapi/service/notification/TitleNotificationProcessor.java new file mode 100644 index 00000000..c8505395 --- /dev/null +++ b/keewe-api/src/main/java/ccc/keeweapi/service/notification/TitleNotificationProcessor.java @@ -0,0 +1,28 @@ +package ccc.keeweapi.service.notification; + +import ccc.keeweapi.dto.notification.NotificationResponse; +import ccc.keewedomain.persistence.domain.notification.Notification; +import ccc.keewedomain.persistence.domain.notification.enums.NotificationCategory; +import ccc.keewedomain.persistence.domain.notification.enums.NotificationContents; +import org.springframework.stereotype.Component; + +@Component +public class TitleNotificationProcessor implements NotificationProcessor { + @Override + public NotificationCategory getCategory() { + return NotificationCategory.TITLE; + } + + @Override + public NotificationResponse process(Notification notification) { + NotificationContents contents = notification.getContents(); + return NotificationResponse.of( + notification.getId(), + contents.getTitle(), + contents.getContents(), + contents.getCategory(), + notification.getReferenceId(), + notification.isRead() + ); + } +} diff --git a/keewe-api/src/main/java/ccc/keeweapi/service/notification/command/.keep b/keewe-api/src/main/java/ccc/keeweapi/service/notification/command/.keep new file mode 100644 index 00000000..e69de29b diff --git a/keewe-api/src/main/java/ccc/keeweapi/service/notification/command/NotificationCommandApiService.java b/keewe-api/src/main/java/ccc/keeweapi/service/notification/command/NotificationCommandApiService.java new file mode 100644 index 00000000..4409e1d7 --- /dev/null +++ b/keewe-api/src/main/java/ccc/keeweapi/service/notification/command/NotificationCommandApiService.java @@ -0,0 +1,36 @@ +package ccc.keeweapi.service.notification.command; + +import ccc.keeweapi.dto.notification.NotificationResponse; +import ccc.keeweapi.service.notification.NotificationProcessor; +import ccc.keeweapi.utils.SecurityUtil; +import ccc.keewedomain.persistence.domain.notification.Notification; +import ccc.keewedomain.persistence.domain.notification.enums.NotificationCategory; +import ccc.keewedomain.service.notification.command.NotificationCommandDomainService; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class NotificationCommandApiService { + private final NotificationCommandDomainService notificationCommandDomainService; + private final Map notificationProcessors; + + public NotificationCommandApiService( + NotificationCommandDomainService notificationCommandDomainService, + List notificationProcessors + ) { + this.notificationCommandDomainService = notificationCommandDomainService; + this.notificationProcessors = notificationProcessors.stream() + .collect(Collectors.toMap(NotificationProcessor::getCategory, notificationProcessor -> notificationProcessor)); + } + + @Transactional + public NotificationResponse markAsRead(Long notificationId) { + Notification notification = notificationCommandDomainService.getByIdWithUserAssert(notificationId, SecurityUtil.getUserId()); + Notification readMarkedNotification = notificationCommandDomainService.save(notification.markAsRead()); + return notificationProcessors.get(readMarkedNotification.getContents().getCategory()) + .process(readMarkedNotification); + } +} diff --git a/keewe-api/src/main/java/ccc/keeweapi/service/notification/query/.keep b/keewe-api/src/main/java/ccc/keeweapi/service/notification/query/.keep new file mode 100644 index 00000000..e69de29b diff --git a/keewe-api/src/main/java/ccc/keeweapi/service/notification/query/NotificationQueryApiService.java b/keewe-api/src/main/java/ccc/keeweapi/service/notification/query/NotificationQueryApiService.java new file mode 100644 index 00000000..65cfa19f --- /dev/null +++ b/keewe-api/src/main/java/ccc/keeweapi/service/notification/query/NotificationQueryApiService.java @@ -0,0 +1,49 @@ +package ccc.keeweapi.service.notification.query; + +import ccc.keeweapi.dto.notification.NotificationResponse; +import ccc.keeweapi.dto.notification.PaginateNotificationResponse; +import ccc.keeweapi.service.notification.NotificationProcessor; +import ccc.keeweapi.utils.SecurityUtil; +import ccc.keewecore.utils.ListUtils; +import ccc.keewedomain.persistence.domain.notification.enums.NotificationCategory; +import ccc.keewedomain.persistence.repository.utils.CursorPageable; +import ccc.keewedomain.service.notification.query.NotificationQueryDomainService; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +@Service +@Slf4j +public class NotificationQueryApiService { + private final NotificationQueryDomainService notificationQueryDomainService; + private final Map notificationProcessors; + + public NotificationQueryApiService( + NotificationQueryDomainService notificationQueryDomainService, + List notificationProcessors + ) { + this.notificationQueryDomainService = notificationQueryDomainService; + this.notificationProcessors = notificationProcessors.stream() + .collect(Collectors.toMap(NotificationProcessor::getCategory, notificationProcessor -> notificationProcessor)); + } + + public PaginateNotificationResponse paginateNotifications(CursorPageable cPage) { + List notificationResponses = notificationQueryDomainService.paginateNotifications(cPage, SecurityUtil.getUser()).stream() + .map(notification -> { + NotificationProcessor notificationProcessor = notificationProcessors.get(notification.getContents().getCategory()); + Assert.notNull(notificationProcessor); + return notificationProcessor.process(notification); + }) + .collect(Collectors.toList()); + + if(notificationResponses.size() >= cPage.getLimit()) { + NotificationResponse lastResponse = ListUtils.getLast(notificationResponses); + return PaginateNotificationResponse.of(lastResponse.getId(), notificationResponses); + } else { + return PaginateNotificationResponse.of(null, notificationResponses); + } + } +} diff --git a/keewe-api/src/test/java/ccc/keeweapi/controller/api/notification/NotificationControllerTest.java b/keewe-api/src/test/java/ccc/keeweapi/controller/api/notification/NotificationControllerTest.java new file mode 100644 index 00000000..42b65461 --- /dev/null +++ b/keewe-api/src/test/java/ccc/keeweapi/controller/api/notification/NotificationControllerTest.java @@ -0,0 +1,114 @@ +package ccc.keeweapi.controller.api.notification; + +import static com.epages.restdocs.apispec.ResourceDocumentation.headerWithName; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import ccc.keeweapi.document.utils.ApiDocumentationTest; +import ccc.keeweapi.dto.notification.NotificationResponse; +import ccc.keeweapi.service.notification.command.NotificationCommandApiService; +import ccc.keeweapi.service.notification.query.NotificationQueryApiService; +import ccc.keewedomain.persistence.domain.notification.enums.NotificationCategory; +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.test.web.servlet.ResultActions; + +public class NotificationControllerTest extends ApiDocumentationTest { + @InjectMocks + NotificationController notificationController; + + @Mock + NotificationCommandApiService notificationCommandApiService; + + @Mock + NotificationQueryApiService notificationQueryApiService; + + @BeforeEach + public void setup(RestDocumentationContextProvider provider) { + super.setup(notificationController, provider); + } + + @Test + @DisplayName("알림 현황 페이지네이션 API 테스트") + void testPaginateNotifications() throws Exception { + // TODO(호성): 조회 연결 후 주석 제거 +// when(notificationQueryApiService.paginateNotifications(any())).thenReturn( +// PaginateNotificationResponse.of(3L, +// List.of(NotificationResponse.of(3L, "내 인사이트에 \n누군가 댓글 남김", "유승훈님이 댓글을 남겼어요.", NotificationCategory.COMMENT, "3", false) +// ) +// )); + + ResultActions resultActions = mockMvc.perform(get("/api/v1/notification") + .param("cursor", Long.toString(3)) + .param("limit", Long.toString(10)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + JWT) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + resultActions.andDo(restDocs.document(resource( + ResourceSnippetParameters.builder() + .description("알림 조회 API입니다.") + .summary("알림 API") + .requestHeaders( + headerWithName("Authorization").description("유저의 JWT")) + .responseFields( + fieldWithPath("message").description("요청 결과 메세지"), + fieldWithPath("code").description("결과 코드"), + fieldWithPath("data.nextCursor").description("다음 알림 커서 ID (null이면 마지막 페이지)"), + fieldWithPath("data.notifications[].id").description("알림 ID"), + fieldWithPath("data.notifications[].title").description("알림 제목"), + fieldWithPath("data.notifications[].contents").description("알림 본문"), + fieldWithPath("data.notifications[].category").description("알림 카테고리"), + fieldWithPath("data.notifications[].referenceId").description("알림 참조 ID"), + fieldWithPath("data.notifications[].read").description("알림 읽었는지 여부") + ) + .tag("Notification") + .build() + ))); + } + + @Test + @DisplayName("알림 읽음 처리 API 테스트") + void testMarkAsRead() throws Exception { + when(notificationCommandApiService.markAsRead(anyLong())).thenReturn( + NotificationResponse.of(3L, "내 인사이트에 \n누군가 댓글 남김", "유승훈님이 댓글을 남겼어요.", NotificationCategory.COMMENT, "3", false) + ); + + ResultActions resultActions = mockMvc.perform(patch("/api/v1/notification/{notificationId}/read", 3L) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + JWT) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + resultActions.andDo(restDocs.document(resource( + ResourceSnippetParameters.builder() + .description("알림 읽음 처리 API입니다.") + .summary("알림 읽음 처리 API") + .requestHeaders( + headerWithName("Authorization").description("유저의 JWT")) + .responseFields( + fieldWithPath("message").description("요청 결과 메세지"), + fieldWithPath("code").description("결과 코드"), + fieldWithPath("data.id").description("알림 ID"), + fieldWithPath("data.title").description("알림 제목"), + fieldWithPath("data.contents").description("알림 본문"), + fieldWithPath("data.category").description("알림 카테고리"), + fieldWithPath("data.referenceId").description("알림 참조 ID"), + fieldWithPath("data.read").description("알림 읽었는지 여부") + ) + .tag("Notification") + .build() + ))); + } +} diff --git a/keewe-core/src/main/java/ccc/keewecore/consts/KeeweRtnConsts.java b/keewe-core/src/main/java/ccc/keewecore/consts/KeeweRtnConsts.java index e769c365..7c58cf7e 100644 --- a/keewe-core/src/main/java/ccc/keewecore/consts/KeeweRtnConsts.java +++ b/keewe-core/src/main/java/ccc/keewecore/consts/KeeweRtnConsts.java @@ -53,6 +53,8 @@ public enum KeeweRtnConsts { ERR480(KeeweRtnGrp.Validation, 480, "타이틀을 획득하지 않았어요."), ERR481(KeeweRtnGrp.Validation, 481, "댓글을 찾을 수 없어요."), + ERR482(KeeweRtnGrp.Validation, 482, "리액션을 찾을 수 없어요."), + ERR483(KeeweRtnGrp.Validation, 483, "알림을 찾을 수 없어요."), ERR501(KeeweRtnGrp.System, 501, "카카오 회원가입 중 내부 오류가 발생했어요."), ERR502(KeeweRtnGrp.System, 502, "네이버 회원가입 중 내부 오류가 발생했어요."), diff --git a/keewe-core/src/main/java/ccc/keewecore/utils/ListUtils.java b/keewe-core/src/main/java/ccc/keewecore/utils/ListUtils.java new file mode 100644 index 00000000..02fa385b --- /dev/null +++ b/keewe-core/src/main/java/ccc/keewecore/utils/ListUtils.java @@ -0,0 +1,9 @@ +package ccc.keewecore.utils; + +import java.util.List; + +public class ListUtils { + public static T getLast(List list) { + return list.get(list.size() - 1); + } +} diff --git a/keewe-domain/src/main/java/ccc/keewedomain/persistence/domain/insight/Reaction.java b/keewe-domain/src/main/java/ccc/keewedomain/persistence/domain/insight/Reaction.java index cdeba835..914fa0b3 100644 --- a/keewe-domain/src/main/java/ccc/keewedomain/persistence/domain/insight/Reaction.java +++ b/keewe-domain/src/main/java/ccc/keewedomain/persistence/domain/insight/Reaction.java @@ -4,6 +4,7 @@ import ccc.keewedomain.persistence.domain.insight.enums.ReactionType; import ccc.keewedomain.persistence.domain.user.User; import lombok.AccessLevel; +import lombok.Getter; import lombok.NoArgsConstructor; import javax.persistence.*; @@ -12,6 +13,7 @@ @Table(name = "reaction") @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter public class Reaction extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/keewe-domain/src/main/java/ccc/keewedomain/persistence/domain/notification/Notification.java b/keewe-domain/src/main/java/ccc/keewedomain/persistence/domain/notification/Notification.java new file mode 100644 index 00000000..11a9ac83 --- /dev/null +++ b/keewe-domain/src/main/java/ccc/keewedomain/persistence/domain/notification/Notification.java @@ -0,0 +1,48 @@ +package ccc.keewedomain.persistence.domain.notification; + +import static javax.persistence.FetchType.LAZY; + +import ccc.keewedomain.persistence.domain.common.BaseTimeEntity; +import ccc.keewedomain.persistence.domain.notification.enums.NotificationContents; +import ccc.keewedomain.persistence.domain.user.User; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import lombok.Getter; + +@Entity +@Table(name = "notification") +@Getter +public class Notification extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "notification_id") + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column(name = "contents") + @Enumerated(EnumType.STRING) + private NotificationContents contents; + + // note. 챌린지 -> 챌린지참여 ID, 타이틀 -> 타이틀 ID, 댓글,답글 -> 댓글 ID, 리액션 -> 인사이트 ID + @Column(name = "reference_id") + private String referenceId; + + @Column(name = "is_read") + private boolean isRead = false; + + public Notification markAsRead() { + this.isRead = true; + return this; + } +} diff --git a/keewe-domain/src/main/java/ccc/keewedomain/persistence/domain/notification/enums/NotificationCategory.java b/keewe-domain/src/main/java/ccc/keewedomain/persistence/domain/notification/enums/NotificationCategory.java new file mode 100644 index 00000000..6c895864 --- /dev/null +++ b/keewe-domain/src/main/java/ccc/keewedomain/persistence/domain/notification/enums/NotificationCategory.java @@ -0,0 +1,10 @@ +package ccc.keewedomain.persistence.domain.notification.enums; + +public enum NotificationCategory { + TITLE, + FOLLOW, + COMMENT, + COMMENT_REPLY, + REACTION, + CHALLENGE +} diff --git a/keewe-domain/src/main/java/ccc/keewedomain/persistence/domain/notification/enums/NotificationContents.java b/keewe-domain/src/main/java/ccc/keewedomain/persistence/domain/notification/enums/NotificationContents.java new file mode 100644 index 00000000..f2713a3b --- /dev/null +++ b/keewe-domain/src/main/java/ccc/keewedomain/persistence/domain/notification/enums/NotificationContents.java @@ -0,0 +1,57 @@ +package ccc.keewedomain.persistence.domain.notification.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum NotificationContents { + // title + 시작이반(NotificationCategory.TITLE, "타이틀 획득!", "키위에 어서오세요. 시작이 반!"), + 위대한_첫_도약(NotificationCategory.TITLE, "타이틀 획득!", "첫 인사이트를 올리셨네요. 위대한 첫 도약!"), + 초보_기록가(NotificationCategory.TITLE, "타이틀 획득!", "꾸준함이 중요하죠. 초보 기록가!"), + 중급_기록가(NotificationCategory.TITLE, "타이틀 획득!", "꾸준함으로 만든 10개의 글. 중급 기록가!"), + 고급_기록가(NotificationCategory.TITLE, "타이틀 획득!", "언제 50개나 쓰셨죠?! 고급 기록가!"), + 인사이트의_신(NotificationCategory.TITLE, "타이틀 획득!", "당신은 그저 God...인사이트의 신!"), + 혼자서도_잘_해요(NotificationCategory.TITLE, "타이틀 획득!", "알아서 척척 하는 당신은, 혼자서도 잘 해요!"), + 두근두근_첫만남(NotificationCategory.TITLE, "타이틀 획득!", "첫 팔로워라니...두근두근 첫만남!"), + 자타공인_인기인(NotificationCategory.TITLE, "타이틀 획득!", "너도나도 닮고싶은 당신. 자타공인 인기인!"), + 피리부는_사나이(NotificationCategory.TITLE, "타이틀 획득!", "100명이 쫓아다니는 인기인. 피리부는 사나이!"), + 정이_많은(NotificationCategory.TITLE, "타이틀 획득!", "40명을 마음에 담았네요. 정이 많은!"), + 참_잘했어요(NotificationCategory.TITLE, "타이틀 획득!", "처음 반응을 받았어요. 참 잘했어요!"), + 아낌없이_주는_나무(NotificationCategory.TITLE, "타이틀 획득!", "처음 반응을 받았어요. 참 잘했어요!"), + 키위새들의_픽(NotificationCategory.TITLE, "타이틀 획득!", "10명의 키위가 좋아해요. 키위새들의 픽!"), + 챌린지_메이커(NotificationCategory.TITLE, "타이틀 획득!", "처음 챌린지를 만드셨네요. 챌린지 메이커!"), + 실패는_성공의_어머니(NotificationCategory.TITLE, "타이틀 획득!", "처음 챌린지를 만드셨네요. 챌린지 메이커!"), + 꺾이지_않는_마음(NotificationCategory.TITLE, "타이틀 획득!", "금새 다음 도전을 하는, 꺾이지 않는 마음!"), + 첫번째_완주(NotificationCategory.TITLE, "타이틀 획득!", "첫 챌린지 성공이네요. 첫번째 완주!"), + 쉬지않고_도전하는(NotificationCategory.TITLE, "타이틀 획득!", "끊임없는 도전! 쉬지않고 도전하는!"), + 혼자보기_아까운(NotificationCategory.TITLE, "타이틀 획득!", "10번이나 공유된, 혼자보기 아까운!"), + 인사이트_수집가(NotificationCategory.TITLE, "타이틀 획득!", "처음 북마크를 저장했네요. 인사이트 수집가!"), + 간직하고_싶은_인사이트(NotificationCategory.TITLE, "타이틀 획득!", "계속계속 보고싶어! 간직하고 싶은 인사이트!"), + 함께하는_즐거움(NotificationCategory.TITLE, "타이틀 획득!", "첫 친구를 초대했네요. 함께하는 즐거움!"), + 마당발(NotificationCategory.TITLE, "타이틀 획득!", "10명이나 초대한 당신은. 마당발!"), + Shall_We_Keewe(NotificationCategory.TITLE, "타이틀 획득!", "모든 타이틀 획득! 이제...Shall We Keewe?"), + + // reaction + 반응(NotificationCategory.REACTION, "내 인사이트에\n누군가 반응 남김", "%s님이 반응을 남겼어요."), + + // follow + 팔로우(NotificationCategory.FOLLOW, "누군가 나를 팔로우", "%s님이 나를 팔로우 했어요."), + + // comment + 댓글(NotificationCategory.COMMENT, "내 인사이트에 누군가 댓글 남김", "%s님이 댓글을 남겼어요."), + 답글(NotificationCategory.COMMENT, "내 인사이트에 누군가 답글 남김", "%s님이 답글을 남겼어요."), + + // challenge + 챌린지_새로운_인사이트(NotificationCategory.CHALLENGE, "%s", "%s님이 이번주 인사이트를 올렸어요."), + 챌린지_신규_참여(NotificationCategory.CHALLENGE, "%s", "%s님이 %s에 합류했어요!"), + 챌린지_종료_전_실패알림(NotificationCategory.CHALLENGE, "%s", "챌린지가 곧 종료돼요."), + 챌린지_종료(NotificationCategory.CHALLENGE, "%s", "챌린지가 종료되었어요."), + 챌린지_종료_후_재도전(NotificationCategory.CHALLENGE, "%s", "챌린지 재도전하러 가기"), + ; + + NotificationCategory category; + String title; + String contents; +} diff --git a/keewe-domain/src/main/java/ccc/keewedomain/persistence/repository/notification/NotificationQueryRepository.java b/keewe-domain/src/main/java/ccc/keewedomain/persistence/repository/notification/NotificationQueryRepository.java new file mode 100644 index 00000000..a94495d3 --- /dev/null +++ b/keewe-domain/src/main/java/ccc/keewedomain/persistence/repository/notification/NotificationQueryRepository.java @@ -0,0 +1,29 @@ +package ccc.keewedomain.persistence.repository.notification; + +import static ccc.keewedomain.persistence.domain.notification.QNotification.notification; + +import ccc.keewedomain.persistence.domain.notification.Notification; +import ccc.keewedomain.persistence.domain.user.User; +import ccc.keewedomain.persistence.repository.utils.CursorPageable; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class NotificationQueryRepository { + private final JPAQueryFactory queryFactory; + + public List paginate(CursorPageable cPage, User user) { + return queryFactory.selectFrom(notification) + .innerJoin(notification.user) + .fetchJoin() + .where(notification.user.eq(user) + .and(notification.id.lt(cPage.getCursor())) + ) + .limit(cPage.getLimit()) + .orderBy(notification.id.desc()) + .fetch(); + } +} diff --git a/keewe-domain/src/main/java/ccc/keewedomain/persistence/repository/notification/NotificationRepository.java b/keewe-domain/src/main/java/ccc/keewedomain/persistence/repository/notification/NotificationRepository.java new file mode 100644 index 00000000..6f8da0a3 --- /dev/null +++ b/keewe-domain/src/main/java/ccc/keewedomain/persistence/repository/notification/NotificationRepository.java @@ -0,0 +1,9 @@ +package ccc.keewedomain.persistence.repository.notification; + +import ccc.keewedomain.persistence.domain.notification.Notification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface NotificationRepository extends JpaRepository { +} diff --git a/keewe-domain/src/main/java/ccc/keewedomain/service/insight/ReactionDomainService.java b/keewe-domain/src/main/java/ccc/keewedomain/service/insight/ReactionDomainService.java index 4286b3b3..c74e811e 100644 --- a/keewe-domain/src/main/java/ccc/keewedomain/service/insight/ReactionDomainService.java +++ b/keewe-domain/src/main/java/ccc/keewedomain/service/insight/ReactionDomainService.java @@ -35,6 +35,10 @@ public class ReactionDomainService { private final UserDomainService userDomainService; private final InsightQueryDomainService insightQueryDomainService; + public Reaction getByIdOrElseThrow(Long id) { + return reactionRepository.findById(id).orElseThrow(() -> new KeeweException(KeeweRtnConsts.ERR482)); + } + public ReactionDto react(ReactionIncrementDto dto) { ReactionAggregationGetDto reactionAggregation = getCurrentReactionAggregation(dto.getInsightId()); diff --git a/keewe-domain/src/main/java/ccc/keewedomain/service/notification/command/NotificationCommandDomainService.java b/keewe-domain/src/main/java/ccc/keewedomain/service/notification/command/NotificationCommandDomainService.java new file mode 100644 index 00000000..9a3b79cc --- /dev/null +++ b/keewe-domain/src/main/java/ccc/keewedomain/service/notification/command/NotificationCommandDomainService.java @@ -0,0 +1,29 @@ +package ccc.keewedomain.service.notification.command; + +import ccc.keewecore.consts.KeeweRtnConsts; +import ccc.keewecore.exception.KeeweException; +import ccc.keewedomain.persistence.domain.notification.Notification; +import ccc.keewedomain.persistence.repository.notification.NotificationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class NotificationCommandDomainService { + private final NotificationRepository notificationRepository; + + public Notification save(Notification notification) { + return notificationRepository.save(notification); + } + + public Notification getByIdWithUserAssert(Long notificationId, Long userId) { + return notificationRepository.findById(notificationId) + .map(notification -> { + if(!notification.getUser().getId().equals(userId)) { + throw new KeeweException(KeeweRtnConsts.ERR404); + } + return notification; + }) + .orElseThrow(() -> new KeeweException(KeeweRtnConsts.ERR483)); + } +} diff --git a/keewe-domain/src/main/java/ccc/keewedomain/service/notification/query/NotificationQueryDomainService.java b/keewe-domain/src/main/java/ccc/keewedomain/service/notification/query/NotificationQueryDomainService.java new file mode 100644 index 00000000..3d766b48 --- /dev/null +++ b/keewe-domain/src/main/java/ccc/keewedomain/service/notification/query/NotificationQueryDomainService.java @@ -0,0 +1,21 @@ +package ccc.keewedomain.service.notification.query; + +import ccc.keewedomain.persistence.domain.notification.Notification; +import ccc.keewedomain.persistence.domain.user.User; +import ccc.keewedomain.persistence.repository.notification.NotificationQueryRepository; +import ccc.keewedomain.persistence.repository.utils.CursorPageable; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class NotificationQueryDomainService { + private final NotificationQueryRepository notificationQueryRepository; + + public List paginateNotifications(CursorPageable cPage, User user) { + return notificationQueryRepository.paginate(cPage, user); + } +} diff --git a/keewe-domain/src/main/resources/ddl/ddl.sql b/keewe-domain/src/main/resources/ddl/ddl.sql index 686c0763..624dc76e 100644 --- a/keewe-domain/src/main/resources/ddl/ddl.sql +++ b/keewe-domain/src/main/resources/ddl/ddl.sql @@ -259,3 +259,16 @@ CREATE TABLE IF NOT EXISTS `report` PRIMARY KEY (report_id), FOREIGN KEY (reporter_id) REFERENCES `user`(user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `notification` +( + notification_id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + contents VARCHAR(30) NOT NULL, + reference_id VARCHAR(30) NOT NULL, + is_read boolean NOT NULL default false, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + + PRIMARY KEY (notification_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/keewe-domain/src/main/resources/ddl/issue-#281_ddl.sql b/keewe-domain/src/main/resources/ddl/issue-#281_ddl.sql new file mode 100644 index 00000000..dc57cfd1 --- /dev/null +++ b/keewe-domain/src/main/resources/ddl/issue-#281_ddl.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS `notification` +( + notification_id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + contents VARCHAR(30) NOT NULL, + reference_id VARCHAR(30) NOT NULL, + is_read boolean NOT NULL default false, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + + PRIMARY KEY (notification_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;