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

알림 기능 구현 #43

Merged
merged 31 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d58bb31
[SCRUM-164] feat: 알림 기본 도메인 및 댓글 작성 이벤트 추가
jinkonu Aug 24, 2024
814fd5b
[SCRUM-164] feat: 메시지 생성 및 전송 로직 추가
jinkonu Aug 25, 2024
0d48022
[SCRUM-164] test: 댓글 측 이벤트 발행 여부 테스트 추가
jinkonu Aug 25, 2024
f17c9fc
[SCRUM-164] test: CommentCommandServiceMockTest로 변경
jinkonu Aug 25, 2024
0acb018
[SCRUM-164] refactor: 좋아요 알림 제거
jinkonu Aug 25, 2024
3df7220
[SCRUM-164] feat: 수신자별 알림 리스트 조회 추가
jinkonu Aug 25, 2024
301b670
[SCRUM-164] feat: 수신자별 알림 리스트 조회 추가
jinkonu Aug 25, 2024
818c50c
[SCRUM-164] test: 알림 커맨드 테스트 추가
jinkonu Aug 25, 2024
35817b4
Merge remote-tracking branch 'origin/feature/SCRUM-164' into feature/…
jinkonu Aug 25, 2024
cf13b37
[SCRUM-164] refactor: 사용자 생성에 fcmToken 필드 추가
jinkonu Aug 25, 2024
2dd9bfb
[SCRUM-164] refactor: FirebaseMessaging의 모킹을 위해 인스턴스를 빈으로 등록 처리
jinkonu Aug 25, 2024
ca2d23d
[SCRUM-164] feat: 전체 알림 API 추가
jinkonu Aug 25, 2024
3796521
[SCRUM-164] fix: 테스트 슈트 실행 시 FirebaseApp 중복 초기화 문제 해결
jinkonu Aug 25, 2024
f6c076f
Merge branch 'refs/heads/dev' into feature/SCRUM-164
jinkonu Aug 25, 2024
16d0cef
[SCRUM-164] settings: Test 파일 내 firebase key 주입 로직 추가
jinkonu Aug 26, 2024
949770b
[SCRUM-164] feat: 알림 기본 도메인 및 댓글 작성 이벤트 추가
jinkonu Aug 24, 2024
9800d6d
[SCRUM-164] feat: 메시지 생성 및 전송 로직 추가
jinkonu Aug 25, 2024
d63c7bd
[SCRUM-164] test: 댓글 측 이벤트 발행 여부 테스트 추가
jinkonu Aug 25, 2024
3a2bcc5
[SCRUM-164] test: CommentCommandServiceMockTest로 변경
jinkonu Aug 25, 2024
1865c3c
[SCRUM-164] refactor: 좋아요 알림 제거
jinkonu Aug 25, 2024
9dbd9c5
[SCRUM-164] feat: 수신자별 알림 리스트 조회 추가
jinkonu Aug 25, 2024
6192c30
[SCRUM-164] test: 알림 커맨드 테스트 추가
jinkonu Aug 25, 2024
052f72a
[SCRUM-164] refactor: 사용자 생성에 fcmToken 필드 추가
jinkonu Aug 25, 2024
05e3027
[SCRUM-164] refactor: FirebaseMessaging의 모킹을 위해 인스턴스를 빈으로 등록 처리
jinkonu Aug 25, 2024
97cbc3b
[SCRUM-164] feat: 전체 알림 API 추가
jinkonu Aug 25, 2024
8ec99bc
[SCRUM-164] fix: 테스트 슈트 실행 시 FirebaseApp 중복 초기화 문제 해결
jinkonu Aug 25, 2024
c343a5b
Merge remote-tracking branch 'origin/feature/SCRUM-164' into feature/…
jinkonu Aug 26, 2024
8de4249
[SCRUM-164] settings: Test 파일 내 firebase key 주입 로직 추가
jinkonu Aug 26, 2024
1ee8f9c
[SCRUM-164] settings: Test.yml 파일 수정
jinkonu Aug 26, 2024
ad72ef0
[SCRUM-164] settings: Test.yml 파일 수정
jinkonu Aug 26, 2024
184aab4
[SCRUM-164] settings: Test.yml 파일 수정
jinkonu Aug 26, 2024
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
13 changes: 13 additions & 0 deletions .github/workflows/Test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ jobs:
with:
redis-version: 6

- name: create-json
id: create-json
uses: jsdaniell/create-json@1.1.2
with:
name: "firebase-key.json"
json: ${{ secrets.FIREBASE_KEY }}

- name: Move firebase-key.json to src/main/resources
run: |
mkdir -p ./src/main/resources
mv firebase-key.json ./src/main/resources/firebase-key.json
shell: bash

- name: Test with Gradle
run: |
./gradlew test
Expand Down
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ dependencies {

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// firebase cloud messaging
implementation 'com.google.firebase:firebase-admin:9.2.0'
}

swaggerSources {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public record AuthSignUpRequest(

@Valid
List<TermsInput> terms

// TODO: fcmToken 추가
) {

public record TermsInput(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
import com.projectlyrics.server.domain.note.entity.Note;
import com.projectlyrics.server.domain.note.exception.NoteNotFoundException;
import com.projectlyrics.server.domain.note.repository.NoteQueryRepository;
import com.projectlyrics.server.domain.notification.domain.event.CommentEvent;
import com.projectlyrics.server.domain.user.entity.User;
import com.projectlyrics.server.domain.user.exception.UserNotFoundException;
import com.projectlyrics.server.domain.user.repository.UserQueryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -30,14 +32,17 @@ public class CommentCommandService {
private final CommentQueryRepository commentQueryRepository;
private final UserQueryRepository userQueryRepository;
private final NoteQueryRepository noteQueryRepository;
private final ApplicationEventPublisher eventPublisher;

public Comment create(CommentCreateRequest request, Long writerId) {
User writer = userQueryRepository.findById(writerId)
.orElseThrow(UserNotFoundException::new);
Note note = noteQueryRepository.findById(request.noteId())
.orElseThrow(NoteNotFoundException::new);

return commentCommandRepository.save(Comment.create(CommentCreate.of(request.content(), writer, note)));
Comment comment = Comment.create(CommentCreate.of(request.content(), writer, note));
eventPublisher.publishEvent(CommentEvent.from(comment));
return commentCommandRepository.save(comment);
}

public Comment update(CommentUpdateRequest request, Long commentId, Long writerId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ public enum ErrorCode {
// Bookmark
BOOKMARK_NOT_FOUND(HttpStatus.NOT_FOUND, "10000", "해당 북마크를 조회할 수 없습니다."),
BOOKMARK_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "10001", "이미 북마크를 추가한 상태입니다."),

// Notification
UNKNOWN_NOTIFICATION_TYPE(HttpStatus.BAD_REQUEST, "11000", "알 수 없는 알림 타입입니다."),
NOTIFICATION_SEND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "11001", "알림 전송에 실패했습니다."),
;

private final HttpStatus responseStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.projectlyrics.server.domain.notification.api;

import com.projectlyrics.server.domain.auth.authentication.AuthContext;
import com.projectlyrics.server.domain.auth.authentication.Authenticated;
import com.projectlyrics.server.domain.notification.api.dto.request.PublicNotificationCreateRequest;
import com.projectlyrics.server.domain.notification.api.dto.response.PublicNotificationCreateResponse;
import com.projectlyrics.server.domain.notification.service.NotificationCommandService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/notifications")
@RequiredArgsConstructor
public class NotificationController {

public final NotificationCommandService notificationCommandService;

@PostMapping("/public")
public ResponseEntity<PublicNotificationCreateResponse> createPublicNotification(
@Authenticated AuthContext authContext,
@RequestBody @Valid PublicNotificationCreateRequest request
) {
notificationCommandService.createPublicNotification(authContext.getId(), request.content());

return ResponseEntity.ok(new PublicNotificationCreateResponse(true));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.projectlyrics.server.domain.notification.api.dto.request;

import jakarta.validation.constraints.NotEmpty;

public record PublicNotificationCreateRequest(
@NotEmpty(message = "알림 내용이 비어 있습니다.")
String content
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.projectlyrics.server.domain.notification.api.dto.response;

public record PublicNotificationCreateResponse(
boolean status
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.projectlyrics.server.domain.notification.domain;

import com.google.firebase.messaging.Message;
import com.projectlyrics.server.domain.comment.domain.Comment;
import com.projectlyrics.server.domain.common.entity.BaseEntity;
import com.projectlyrics.server.domain.note.entity.Note;
import com.projectlyrics.server.domain.notification.domain.event.CommentEvent;
import com.projectlyrics.server.domain.notification.domain.event.PublicEvent;
import com.projectlyrics.server.domain.user.entity.User;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@Table(name = "notifications")
@EqualsAndHashCode(of = "id", callSuper = false)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Notification extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private NotificationType type;
private String content;

@ManyToOne(fetch = FetchType.LAZY)
private User sender;
@ManyToOne(fetch = FetchType.LAZY)
private User receiver;

@ManyToOne(fetch = FetchType.LAZY)
private Note note;
@ManyToOne(fetch = FetchType.LAZY)
private Comment comment;

private Notification(
Long id,
NotificationType type,
String content,
User sender,
User receiver,
Note note,
Comment comment
) {
this.id = id;
this.type = type;
this.sender = sender;
this.content = content;
this.receiver = receiver;
this.note = note;
this.comment = comment;
}

public static Notification create(CommentEvent event) {
return new Notification(
null,
NotificationType.COMMENT_ON_NOTE,
null,
event.sender(),
event.receiver(),
event.note(),
event.comment()
);
}

public static Notification create(PublicEvent event) {
return new Notification(
null,
NotificationType.PUBLIC,
event.content(),
event.sender(),
event.receiver(),
null,
null
);
}

public Message getMessage() {
Message.Builder builder = Message.builder()
.setToken(receiver.getFcmToken());

switch (type) {
case COMMENT_ON_NOTE:
return builder
.putData("type", type.name())
.putData("senderId", sender.getId().toString())
.putData("senderNickname", sender.getNickname().getValue())
.putData("noteId", note.getId().toString())
.putData("noteTitle", note.getContent())
.build();
default:
throw new IllegalArgumentException("Invalid notification type");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.projectlyrics.server.domain.notification.domain;

public enum NotificationType {
COMMENT_ON_NOTE,
REPORT,
PUBLIC,
;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.projectlyrics.server.domain.notification.domain.event;

import com.projectlyrics.server.domain.comment.domain.Comment;
import com.projectlyrics.server.domain.note.entity.Note;
import com.projectlyrics.server.domain.user.entity.User;

public record CommentEvent(
User sender,
User receiver,
Note note,
Comment comment
) {

public static CommentEvent from(Comment comment) {
return new CommentEvent(
comment.getWriter(),
comment.getNote().getPublisher(),
comment.getNote(),
comment
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.projectlyrics.server.domain.notification.domain.event;

import com.projectlyrics.server.domain.user.entity.User;

public record PublicEvent(
User sender,
User receiver,
String content
) {

public static PublicEvent of(String content, User sender, User receiver) {
return new PublicEvent(sender, receiver, content);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.projectlyrics.server.domain.notification.exception;

import com.projectlyrics.server.domain.common.message.ErrorCode;
import com.projectlyrics.server.global.exception.FeelinException;

public class FailedToSendNotificationException extends FeelinException {

public FailedToSendNotificationException() {
super(ErrorCode.NOTIFICATION_SEND_FAILED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.projectlyrics.server.domain.notification.exception;

import com.projectlyrics.server.domain.common.message.ErrorCode;
import com.projectlyrics.server.global.exception.FeelinException;

public class UnknownNotificationException extends FeelinException {

public UnknownNotificationException() {
super(ErrorCode.UNKNOWN_NOTIFICATION_TYPE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.projectlyrics.server.domain.notification.repository;

import com.projectlyrics.server.domain.notification.domain.Notification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface NotificationCommandRepository extends JpaRepository<Notification, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.projectlyrics.server.domain.notification.repository;

import com.projectlyrics.server.domain.notification.domain.Notification;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;

public interface NotificationQueryRepository {

Slice<Notification> findAllByReceiverId(Long receiverId, Long cursorId, Pageable pageable);
Slice<Notification> findAllBySenderId(Long senderId, Long cursorId, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.projectlyrics.server.domain.notification.repository.impl;

import com.projectlyrics.server.domain.common.util.QueryDslUtils;
import com.projectlyrics.server.domain.notification.domain.Notification;
import com.projectlyrics.server.domain.notification.repository.NotificationQueryRepository;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.stereotype.Repository;

import java.util.List;

import static com.projectlyrics.server.domain.notification.domain.QNotification.notification;

@Repository
@RequiredArgsConstructor
public class QueryDslNotificationQueryRepository implements NotificationQueryRepository {

private final JPAQueryFactory jpaQueryFactory;

@Override
public Slice<Notification> findAllByReceiverId(Long receiverId, Long cursorId, Pageable pageable) {
List<Notification> content = jpaQueryFactory
.selectFrom(notification)
.leftJoin(notification.sender).fetchJoin()
.leftJoin(notification.receiver).fetchJoin()
.leftJoin(notification.note).fetchJoin()
.leftJoin(notification.comment).fetchJoin()
.where(
notification.receiver.id.eq(receiverId),
QueryDslUtils.gtCursorId(cursorId, notification.id),
notification.deletedAt.isNull()
)
.orderBy(notification.id.desc())
.limit(pageable.getPageSize() + 1)
.fetch();

return new SliceImpl<>(content, pageable, QueryDslUtils.checkIfHasNext(pageable, content));
}

@Override
public Slice<Notification> findAllBySenderId(Long senderId, Long cursorId, Pageable pageable) {
List<Notification> content = jpaQueryFactory
.selectFrom(notification)
.leftJoin(notification.sender).fetchJoin()
.leftJoin(notification.receiver).fetchJoin()
.leftJoin(notification.note).fetchJoin()
.leftJoin(notification.comment).fetchJoin()
.where(
notification.sender.id.eq(senderId),
QueryDslUtils.gtCursorId(cursorId, notification.id),
notification.deletedAt.isNull()
)
.orderBy(notification.id.desc())
.limit(pageable.getPageSize() + 1)
.fetch();

return new SliceImpl<>(content, pageable, QueryDslUtils.checkIfHasNext(pageable, content));
}
}
Loading
Loading