Skip to content

Commit

Permalink
feat: #39 notice event listener 등록
Browse files Browse the repository at this point in the history
  • Loading branch information
psychology50 committed Feb 27, 2024
1 parent e408684 commit 55ad7ff
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,22 @@
import lombok.Builder;

import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
* FCM Push Notification Event를 위한 Data Type Class
*
* @param memberId Long : 푸시 알림을 받을 회원 ID
* @param noticeType {@link NoticeType} : push 알림을 위한 알림 타입
* @param notificationType {@link NotificationType} : DB에 저장될 알림 타입
* @param title String : 푸시 알림 제목
* @param content String : 푸시 알림 내용. {@link NoticeType}에 따라 동적으로 생성
* @param imageUrl String : 푸시 알림 이미지 URL (optional)
* @param data Map<{@link NotificationDataKey}, String> : 푸시 알림에 포함될 데이터
*/
@Builder
public record NoticeEvent(
Long memberId,
NoticeType noticeType,
NotificationType notificationType,
String title,
Expand All @@ -18,24 +30,47 @@ public record NoticeEvent(
Map<NotificationDataKey, String> data
) {
public Notification toEntity() {
String fromId = data.getOrDefault(NotificationDataKey.FROM_ID, null);
String toId = data.getOrDefault(NotificationDataKey.TO_ID, null);
String domainId = data.getOrDefault(NotificationDataKey.DOMAIN_ID, null);
String subjectId = data.getOrDefault(NotificationDataKey.SUBJECT_ID, null);
Map<String, Long> idFields = getIdFields();

return Notification.builder()
.title(title)
.content(content)
.content(getFormattedContent())
.imageUrl(imageUrl)
.ctype(notificationType)
.fromId(Objects.isNull(fromId) ? null : Long.parseLong(fromId))
.toId(Objects.isNull(toId) ? null : Long.parseLong(toId))
.domainId(Objects.isNull(domainId) ? null : Long.parseLong(domainId))
.subjectId(Objects.isNull(subjectId) ? null : Long.parseLong(subjectId))
.fromId(idFields.getOrDefault(NotificationDataKey.FROM_ID.getField(), null))
.toId(idFields.getOrDefault(NotificationDataKey.TO_ID.getField(), null))
.domainId(idFields.getOrDefault(NotificationDataKey.DOMAIN_ID.getField(), null))
.subjectId(idFields.getOrDefault(NotificationDataKey.SUBJECT_ID.getField(), null))
.build();
}

public String getValueFromNotificationDataKey(NotificationDataKey key) {
return data.getOrDefault(key, null);
/**
* data의 정보를 {@link NoticeType}에 따라 동적으로 생성된 content를 반환합니다.
* @return String : 동적으로 생성된 content
*/
public String getFormattedContent() {
return noticeType.createFormattedContent(getNameFields());
}

private Map<String, Long> getIdFields() {
return data.entrySet().stream()
.filter(e -> e.getKey().isIdField())
.collect(
Collectors.toMap(
e -> e.getKey().getField(),
e -> Long.parseLong(e.getValue())
)
);
}

private Map<String, String> getNameFields() {
return data.entrySet().stream()
.filter(e -> e.getKey().isNameField())
.collect(
Collectors.toMap(
e -> e.getKey().getField(),
Map.Entry::getValue
)
);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package kr.co.fitapet.api.common.event.notification;

import kr.co.fitapet.domain.domains.device.domain.DeviceToken;
import kr.co.fitapet.domain.domains.device.exception.DeviceTokenErrorCode;
import kr.co.fitapet.domain.domains.device.exception.DeviceTokenErrorException;
import kr.co.fitapet.domain.domains.device.service.DeviceTokenSearchService;
import kr.co.fitapet.domain.domains.notification.domain.Notification;
import kr.co.fitapet.domain.domains.notification.service.NotificationSaveService;
import kr.co.fitapet.domain.domains.notification.type.NotificationType;
import kr.co.fitapet.infra.client.fcm.NotificationDataKey;
import kr.co.fitapet.infra.common.event.NotificationSingleEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
Expand All @@ -23,13 +28,41 @@ public class NoticeEventHandler {
private final DeviceTokenSearchService deviceTokenSearchService;
private final NotificationSaveService notificationSaveService;

/**
* 일반적인 push notification 이벤트{@link NoticeEvent} 핸들러입니다.
* <br/>
* 알림 이벤트를 받아서 알림을 저장하고, 해당 알림을 전송할 대상을 조회하여 {@link Notification}를 등록합니다.
* <br/>
* {@link TransactionalEventListener}를 통해 이벤트를 발행하는 트랜잭션 커밋 이후에 핸들러가 실행됩니다.
* <br/>
* 이벤트를 발행한 트랜잭션과 별개의 트랜잭션으로 실행됩니다.
*
* @param event {@link NoticeEvent}
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
public void handleNoticeEvent(NoticeEvent event) {
log.info("handleNoticeEvent: {}", event);
// notice 저장
// notice 전송할 대상 조회 (deviceToken 리스트)
// deviceToken 리스트 전체 NotificationEvent 등록
Notification notification = event.toEntity();
notificationSaveService.save(notification);

List<DeviceToken> deviceTokens = deviceTokenSearchService.findDeviceTokensByMemberId(event.memberId());
if (deviceTokens.isEmpty()) {
log.warn("No device tokens found for member: {}", event.memberId());
throw new DeviceTokenErrorException(DeviceTokenErrorCode.NOT_FOUND_DEVICE_TOKEN_ERROR);
}

deviceTokens.forEach(deviceToken -> {
eventPublisher.publishEvent(
NotificationSingleEvent.builder()
.deviceToken(deviceToken.getDeviceToken())
.title(event.title())
.content(event.getFormattedContent())
.imageUrl(event.imageUrl())
.data(event.data())
.build()
);
});
}

/**
Expand All @@ -55,6 +88,21 @@ public void handleAnnouncementEvent(AnnouncementEvent event) {
Long toId = deviceToken.getMember().getId();

notifications.add(event.toEntity(toId));
Map<NotificationDataKey, String> data = Map.of(
NotificationDataKey.NOTICE_TYPE, NotificationType.ANNOUNCEMENT.name(),
NotificationDataKey.TO_ID, deviceToken.getMember().getId().toString(),
NotificationDataKey.TO_NAME, deviceToken.getMember().getName()
);

eventPublisher.publishEvent(
NotificationSingleEvent.builder()
.deviceToken(token)
.title(event.title())
.content(event.content())
.imageUrl(event.imageUrl())
.data(data)
.build()
);
});
notificationSaveService.saveAll(notifications);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package kr.co.fitapet.domain.domains.device.exception;

import kr.co.fitapet.common.execption.BaseErrorCode;
import kr.co.fitapet.common.execption.CausedBy;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

import static kr.co.fitapet.common.execption.StatusCode.NOT_FOUND;

@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public enum DeviceTokenErrorCode implements BaseErrorCode {
/* 404 NOT FOUND */
NOT_FOUND_DEVICE_TOKEN_ERROR(NOT_FOUND.getCode(), "존재하지 않는 디바이스 토큰입니다.");

private final int code;
private final String message;

@Override
public CausedBy causedBy() {
return CausedBy.of(code, name(), message);
}

@Override
public String getExplainError() throws NoSuchFieldError {
return message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package kr.co.fitapet.domain.domains.device.exception;

import kr.co.fitapet.common.execption.BaseErrorCode;
import kr.co.fitapet.common.execption.CausedBy;
import kr.co.fitapet.common.execption.GlobalErrorException;

public class DeviceTokenErrorException extends GlobalErrorException {
private final DeviceTokenErrorCode errorCode;

public DeviceTokenErrorException(DeviceTokenErrorCode errorCode) {
super(errorCode);
this.errorCode = errorCode;
}

public CausedBy causedBy() {
return errorCode.causedBy();
}

public BaseErrorCode getErrorCode() {
return errorCode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,19 @@ public enum NotificationDataKey {
SUBJECT_NAME("subjectName"),
;
private final String field;
private static final List<NotificationDataKey> idFields = List.of(FROM_ID, TO_ID, DOMAIN_ID, SUBJECT_ID);
private static final List<NotificationDataKey> nameFields = List.of(FROM_NAME, TO_NAME, DOMAIN_NAME, SUBJECT_NAME);

@JsonValue
public String getField() {
return field;
}

public static List<String> getIdFields() {
return List.of("fromId", "toId", "domainId", "subjectId");
public boolean isIdField() {
return idFields.contains(this);
}

public static List<String> getNameFields() {
return List.of("fromName", "toName", "domainName", "subjectName");
public boolean isNameField() {
return nameFields.contains(this);
}
}

0 comments on commit 55ad7ff

Please sign in to comment.