Skip to content

Commit

Permalink
feat(notification): add push notification feature (#167)
Browse files Browse the repository at this point in the history
* build(gradle): add expo-server-sdk dependency

* feat(notification): add push notification sending service

* feat(notification): modify meeting events to include push notification events

* feat(notification): add scheduler for recommended push notifications

* test(notification): add notification scheduler test

* test(notification): add NotificationHandleServiceTest

* test(notification): add NotificationServiceTest

* test(notification): rename NotificationServiceTest functions

* fix(notification): modify query's where condition

* test(notification): add PushTokenRepositoryTest

* chore: add jackson dependencies

* docs: add swagger api

---------

Co-authored-by: KAispread <[email protected]>
  • Loading branch information
Chaerim1001 and KAispread authored Nov 14, 2023
1 parent 6e85e09 commit 1efbe26
Show file tree
Hide file tree
Showing 20 changed files with 621 additions and 146 deletions.
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ dependencies {

// Component that test code convert to Swagger file and include restDocs Api
testImplementation('com.epages:restdocs-api-spec-mockmvc:0.18.2') //2.2


// Json Serializer
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0'
implementation 'com.fasterxml.jackson.core:jackson-core:2.15.0'
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.15.0'

// expo push notification
implementation('io.github.jav:expo-server-sdk:1.1.0')
}

// Querydsl
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/e2i/wemeet/config/common/SchedulerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.e2i.wemeet.config.common;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
public class SchedulerConfig {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.e2i.wemeet.config.notification;

import io.github.jav.exposerversdk.PushClient;
import io.github.jav.exposerversdk.PushClientException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class NotificationConfig {

@Bean
public PushClient pushClient() throws PushClientException {
return new PushClient();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ public interface MeetingRepository extends JpaRepository<Meeting, Long>, Meeting
join MeetingRequest mr on mr.partnerTeam = pt and mr.team = t
where mr.meetingRequestId = :meetingRequestId
""")
List<LocalDateTime> findCreatedAtByMeetingRequestId(@Param("meetingRequestId") final Long meetingRequestId);
List<LocalDateTime> findCreatedAtByMeetingRequestId(
@Param("meetingRequestId") final Long meetingRequestId);

/*
* 팀장의 pushToken 조회
* */
@Query("""
select p.token
from PushToken p
join Team t on t.teamLeader.memberId = p.member.memberId and t.deletedAt is null
where t.teamId = :teamId
""")
Optional<String> findLeaderPushTokenById(@Param("teamId") final Long teamId);

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.e2i.wemeet.domain.notification;

import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
Expand All @@ -10,4 +11,15 @@ public interface PushTokenRepository extends JpaRepository<PushToken, String> {
@Query("select p from PushToken p where p.token = :token")
Optional<PushToken> findByToken(@Param("token") String token);

}
@Query("select p.token from PushToken p where p.member.memberId is not null")
List<String> findAllMemberTokens();

@Query("""
select p.token
from PushToken p
join Member m on m.memberId = p.member.memberId
where p.member.memberId is not null
and m.role = 'USER'
""")
List<String> findTokensOfMemberWithoutTeam();
}
11 changes: 8 additions & 3 deletions src/main/java/com/e2i/wemeet/service/meeting/MeetingEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@

import com.e2i.wemeet.domain.cost.Spent;
import com.e2i.wemeet.service.cost.SpendEvent;
import com.e2i.wemeet.service.notification.event.NotificationEvent;
import com.e2i.wemeet.service.sns.SnsEvent;

public record MeetingEvent(
SnsEvent snsEvent,
SpendEvent spendEvent
SpendEvent spendEvent,
NotificationEvent notificationEvent

) {

public static MeetingEvent of(String receivePhoneNumber, String message, Spent type, Long memberId) {
public static MeetingEvent of(String receivePhoneNumber, String token, String message,
Spent type, Long memberId) {
SnsEvent snsEvent = new SnsEvent(receivePhoneNumber, message);
SpendEvent spendEvent = new SpendEvent(type, memberId);
return new MeetingEvent(snsEvent, spendEvent);
NotificationEvent notificationEvent = new NotificationEvent(token, message);
return new MeetingEvent(snsEvent, spendEvent, notificationEvent);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ public Long sendRequest(final SendMeetingRequestDto requestDto, final Long membe
MeetingRequest request = meetingRequestRepository.save(meetingRequest);

// 이벤트 발행
publishMeetingEvent(getMeetingRequestMessage(), memberLeaderId, partnerTeam, MEETING_REQUEST);
publishMeetingEvent(getMeetingRequestMessage(), memberLeaderId, partnerTeam,
MEETING_REQUEST);
return request.getMeetingRequestId();
}

Expand Down Expand Up @@ -110,7 +111,8 @@ public Long acceptRequest(final Long memberLeaderId, final Long meetingRequestId
// 미팅 성사 이벤트 발행
Team myTeam = meetingRequest.getTeam();
String leaderNickname = meetingRequest.getPartnerTeam().getTeamLeader().getNickname();
publishMeetingEvent(getMeetingAcceptMessage(leaderNickname), memberLeaderId, myTeam, MEETING_ACCEPT);
publishMeetingEvent(getMeetingAcceptMessage(leaderNickname), memberLeaderId, myTeam,
MEETING_ACCEPT);

return saveMeeting(meetingRequest).getMeetingId();
}
Expand Down Expand Up @@ -181,8 +183,11 @@ private void publishMeetingEvent(final String message, final Long memberLeaderId
final Team targetTeam, final Spent spent) {
String leaderPhoneNumber = meetingRepository.findLeaderPhoneNumberById(
targetTeam.getTeamId());
String leaderPushToken = meetingRepository.findLeaderPushTokenById(
targetTeam.getTeamId()).orElse(null);

eventPublisher.publishEvent(
MeetingEvent.of(leaderPhoneNumber, message, spent, memberLeaderId)
MeetingEvent.of(leaderPhoneNumber, leaderPushToken, message, spent, memberLeaderId)
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.e2i.wemeet.service.notification;


import java.util.List;

public interface NotificationHandleService {

/*
* Body 미포함 푸시 알림 전송
*/
void sendPushNotification(List<String> tokens, String title);

/*
* Body 포함 푸시 알림 전송
*/
void sendPushNotification(List<String> tokens, String title, String body);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.e2i.wemeet.service.notification;

import io.github.jav.exposerversdk.ExpoPushMessage;
import io.github.jav.exposerversdk.PushClient;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;


@RequiredArgsConstructor
@Service
public class NotificationHandleServiceImpl implements NotificationHandleService {

private final PushClient client;

@Override
public void sendPushNotification(List<String> tokens, String title) {
sendPushNotification(tokens, title, null);
}

@Override
public void sendPushNotification(List<String> tokens, String title, String body) {
ExpoPushMessage expoPushMessage = createExpoPushMessage(tokens, title, body);
client.sendPushNotificationsAsync(List.of(expoPushMessage));
}

private ExpoPushMessage createExpoPushMessage(List<String> tokens, String title, String body) {
ExpoPushMessage expoPushMessage = new ExpoPushMessage();
expoPushMessage.setTo(tokens);
expoPushMessage.setTitle(title);

if (body != null && !body.isBlank()) {
expoPushMessage.setBody(body);
}

return expoPushMessage;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.e2i.wemeet.service.notification;

import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class NotificationScheduler {

private final NotificationService notificationService;
private static final String TITLE = "기다리고 기다리던 11시 11분이야!";
private static final String BODY = "오늘의 추천 친구들을 확인해 봐! 🤩";

// 매일 23시 11분에 실행
@Scheduled(cron = "0 11 23 * * ?")
public void sendPushNotificationForSuggestion() {
notificationService.sendToAllMembers(TITLE, BODY);
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.e2i.wemeet.service.notification;

public interface NotificationService {

/*
* 전체 사용자 푸시 알림 전송
*/
void sendToAllMembers(String title, String body);

/*
* 팀이 없는 사용자 푸시 알림 전송
*/
void sendToMembersWithoutTeam(String title, String body);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.e2i.wemeet.service.notification;

import com.e2i.wemeet.domain.notification.PushTokenRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class NotificationServiceImpl implements NotificationService {

private final NotificationHandleService notificationHandleService;
private final PushTokenRepository pushTokenRepository;

@Override
public void sendToAllMembers(String title, String body) {
List<String> tokens = pushTokenRepository.findAllMemberTokens();
notificationHandleService.sendPushNotification(tokens, title, body);
}

@Override
public void sendToMembersWithoutTeam(String title, String body) {
List<String> tokens = pushTokenRepository.findTokensOfMemberWithoutTeam();
notificationHandleService.sendPushNotification(tokens, title, body);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.e2i.wemeet.service.notification.event;

public interface NotificatioEventService {

/*
* 이벤트 전용 푸시 알림 전송
*/
void sendForNotificationEvent(NotificationEvent notificationEvent);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.e2i.wemeet.service.notification.event;

public record NotificationEvent(String token, String title) {

public static NotificationEvent of(String token, String title) {
return new NotificationEvent(token, title);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.e2i.wemeet.service.notification.event;

import com.e2i.wemeet.service.meeting.MeetingEvent;
import com.e2i.wemeet.service.notification.NotificationHandleService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class NotificationEventServiceImpl implements NotificatioEventService {

private final NotificationHandleService notificationHandleService;

@EventListener(classes = MeetingEvent.class)
public void sendForNotificationEvent(final MeetingEvent event) {
sendForNotificationEvent(event.notificationEvent());
}

@EventListener(classes = NotificationEvent.class)
@Override
public void sendForNotificationEvent(final NotificationEvent notificationEvent) {
if (notificationEvent.token() == null) {
return;
}

notificationHandleService.sendPushNotification(List.of(notificationEvent.token()),
notificationEvent.title());
}
}
Loading

0 comments on commit 1efbe26

Please sign in to comment.