Skip to content

Commit

Permalink
✨ Feature: FCM 알림 기능 구현 (#94)
Browse files Browse the repository at this point in the history
* ✨ Feature: FcmToken 도메인 작성

* ✨ Feature: FCM 알림 기능 구현
  • Loading branch information
ahnsugyeong authored Jun 1, 2024
1 parent a913553 commit b8149f2
Show file tree
Hide file tree
Showing 20 changed files with 286 additions and 4 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ moodoodle-batch/src/main/resources/*.yml
moodoodle-common/src/main/resources/*.yml
moodoodle-domain/src/main/resources/*.yml
moodoodle-infrastructure/src/main/resources/*.yml
moodoodle-notification/src/main/resources/*.yml
1 change: 1 addition & 0 deletions moodoodle-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ dependencies {
implementation project(':moodoodle-common')
implementation project(':moodoodle-domain')
implementation project(':moodoodle-infrastructure')
implementation project(':moodoodle-notification')

// spring boot
implementation 'org.springframework.boot:spring-boot-starter-web'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
public class MoodoodleApiApplication {

public static void main(String[] args) {
System.setProperty("spring.config.name", "application-api, application-domain, application-infrastructure");
System.setProperty("spring.config.name",
"application-api, application-domain, application-infrastructure, application-notification");
SpringApplication.run(MoodoodleApiApplication.class, args);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import zzangdol.user.implement.UserCommandService;
import zzangdol.user.presentation.dto.request.PushNotificationRequest;
import zzangdol.user.presentation.dto.request.UserInfoUpdateRequest;
import zzangdol.user.presentation.dto.response.UserInfoResponse;
import zzangdol.user.domain.User;
Expand All @@ -25,4 +26,8 @@ public boolean withDrawUser(User user) {
userCommandService.withDrawUser(user);
return true;
}

public void handlePushNotifications(User user, PushNotificationRequest request) {
userCommandService.handlePushNotifications(user, request);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package zzangdol.user.implement;

import zzangdol.user.domain.User;
import zzangdol.user.presentation.dto.request.PushNotificationRequest;
import zzangdol.user.presentation.dto.request.UserInfoUpdateRequest;

public interface UserCommandService {

User updateUserInfo(User user, UserInfoUpdateRequest request);

User handlePushNotifications(User user, PushNotificationRequest request);

void withDrawUser(User user);

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package zzangdol.user.implement;

import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import zzangdol.diary.dao.DiaryRepository;
import zzangdol.notification.dao.FcmTokenRepository;
import zzangdol.notification.domain.FcmToken;
import zzangdol.report.dao.ReportRepository;
import zzangdol.scrap.dao.ScrapRepository;
import zzangdol.user.dao.UserRepository;
import zzangdol.user.domain.User;
import zzangdol.user.presentation.dto.request.PushNotificationRequest;
import zzangdol.user.presentation.dto.request.UserInfoUpdateRequest;

@RequiredArgsConstructor
Expand All @@ -19,6 +23,7 @@ public class UserCommandServiceImpl implements UserCommandService {
private final ReportRepository reportRepository;
private final DiaryRepository diaryRepository;
private final UserRepository userRepository;
private final FcmTokenRepository fcmTokenRepository;

@Override
public User updateUserInfo(User user, UserInfoUpdateRequest request) {
Expand All @@ -27,6 +32,40 @@ public User updateUserInfo(User user, UserInfoUpdateRequest request) {
return user;
}

@Override
public User handlePushNotifications(User user, PushNotificationRequest request) {
user.updatePushNotificationsEnabled(request.getPushNotificationsEnabled());

if (Boolean.TRUE.equals(request.getPushNotificationsEnabled())) {
storeFcmToken(request.getFcmToken(), user);
} else {
abortFcmToken(request.getFcmToken(), user);
}

return userRepository.save(user);
}

public void storeFcmToken(String token, User user) {
Optional<FcmToken> optionalFcmToken = fcmTokenRepository.findByUserAndToken(user, token);

optionalFcmToken.ifPresentOrElse(
fcmToken -> {},
() -> fcmTokenRepository.save(FcmToken.builder()
.token(token)
.user(user)
.build())
);
}

public void abortFcmToken(String token, User user) {
Optional<FcmToken> optionalFcmToken = fcmTokenRepository.findByUserAndToken(user, token);

optionalFcmToken.ifPresentOrElse(
fcmTokenRepository::delete,
() -> {}
);
}

@Override
public void withDrawUser(User user) {
scrapRepository.deleteByUser(user);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
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;
import zzangdol.FCMService;
import zzangdol.global.annotation.AuthUser;
import zzangdol.response.ResponseDto;
import zzangdol.user.business.UserFacade;
import zzangdol.user.domain.User;
import zzangdol.user.presentation.dto.request.PushNotificationRequest;
import zzangdol.user.presentation.dto.request.UserInfoUpdateRequest;
import zzangdol.user.presentation.dto.response.UserInfoResponse;
import zzangdol.response.ResponseDto;
import zzangdol.user.domain.User;

@RequiredArgsConstructor
@ApiResponse(responseCode = "2000", description = "성공")
Expand All @@ -25,6 +28,7 @@
public class UserController {

private final UserFacade userFacade;
private final FCMService fcmService;

@Operation(
summary = "사용자 정보 조회 🔑",
Expand All @@ -44,6 +48,30 @@ public ResponseDto<UserInfoResponse> updateUserInfo(@AuthUser User user, @Reques
return ResponseDto.onSuccess(userFacade.updateUserInfo(user, request));
}

@Operation(
summary = "Push 알림 허용 / 거부 🔑",
description = "사용자가 Push 알림을 허용하거나 거부합니다."
)
@PatchMapping("/push-notifications")
public ResponseDto<Void> handlePushNotifications(@AuthUser User user, @RequestBody PushNotificationRequest request) {
userFacade.handlePushNotifications(user, request);
return ResponseDto.onSuccess();
}

@Operation(
summary = "테스트 푸시 알림 전송 🔑",
description = "특정 사용자의 FCM 토큰으로 푸시 알림을 즉시 전송합니다."
)
@PostMapping("/send-test-notification")
public ResponseDto<Void> sendTestNotification(@RequestBody PushNotificationRequest request) {
try {
fcmService.sendNotification(request.getFcmToken(), "테스트 title", "테스트 body");
} catch (Exception e) {
e.printStackTrace();
}
return ResponseDto.onSuccess();
}

@Operation(
summary = "회원 탈퇴 🔑",
description = "현재 로그인된 사용자의 계정을 탈퇴하고, 모든 관련 데이터를 삭제합니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package zzangdol.user.presentation.dto.request;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class PushNotificationRequest {

private Boolean pushNotificationsEnabled;
private String fcmToken;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package zzangdol.notification.dao;

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import zzangdol.notification.domain.FcmToken;
import zzangdol.user.domain.User;

public interface FcmTokenRepository extends JpaRepository<FcmToken, Long> {

Optional<FcmToken> findByUserAndToken(User user, String token);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package zzangdol.notification.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import zzangdol.user.domain.User;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class FcmToken {

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

private String token;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;

@Builder
public FcmToken(String token, User user) {
this.token = token;
this.user = user;
}
}
7 changes: 7 additions & 0 deletions moodoodle-domain/src/main/java/zzangdol/user/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class User extends BaseTimeEntity implements UserDetails {
private String email;
private String password;
private String nickname;
private boolean pushNotificationsEnabled;
private LocalTime notificationTime;
private Boolean isRead;

Expand Down Expand Up @@ -98,4 +99,10 @@ public void updateNotificationTime(LocalTime notificationTime) {
public void setRead(Boolean read) {
isRead = read;
}

public void updatePushNotificationsEnabled(Boolean pushNotificationsEnabled) {
if (pushNotificationsEnabled != null) {
this.pushNotificationsEnabled = pushNotificationsEnabled;
}
}
}
21 changes: 21 additions & 0 deletions moodoodle-notification/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
dependencies {
implementation project(':moodoodle-domain')

// spring boot
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-web'

// fcm
implementation 'com.google.firebase:firebase-admin:8.0.0'
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.2.2'

}

task copyYML(type: Copy) {
copy {
from '../moodoodle-submodule/notification'
include "*.yml"
into 'src/main/resources'
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
37 changes: 37 additions & 0 deletions moodoodle-notification/src/main/java/zzangdol/FCMService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package zzangdol;

import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.Message;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import zzangdol.notification.dao.FcmTokenRepository;
import zzangdol.notification.domain.FcmToken;

@Slf4j
@RequiredArgsConstructor
@Service
public class FCMService {

private final FcmTokenRepository fcmTokenRepository;

public void sendNotification(String token, String title, String body) throws Exception {
Message message = Message.builder()
.putData("title", title)
.putData("body", body)
.setToken(token)
.build();

String response = FirebaseMessaging.getInstance().send(message);
System.out.println("Successfully sent message: " + response);
}

public void sendNotificationToAllUsers(String title, String body) throws Exception {
List<FcmToken> tokens = fcmTokenRepository.findAll();
for (FcmToken token : tokens) {
sendNotification(token.getToken(), title, body);
}
}

}
34 changes: 34 additions & 0 deletions moodoodle-notification/src/main/java/zzangdol/FirebaseConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package zzangdol;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import jakarta.annotation.PostConstruct;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Base64;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FirebaseConfig {

@Value("${fcm.key}")
private String fcmJson;

@PostConstruct
public void initFirebase() {
String base64String = fcmJson;
byte[] decodedBytes = Base64.getDecoder().decode(base64String);
InputStream credentialStream = new ByteArrayInputStream(decodedBytes);

try {
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(credentialStream))
.build();
FirebaseApp.initializeApp(options);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package zzangdol;

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

@RequiredArgsConstructor
@Component
public class NotificationScheduler {

private final FCMService fcmService;

@Scheduled(cron = "0 0 21 * * ?")
public void scheduleDailyNotification() {
try {
String title = "Daily Reminder";
String body = "This is your daily reminder!";
fcmService.sendNotificationToAllUsers(title, body);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Loading

0 comments on commit b8149f2

Please sign in to comment.