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

✨ Feature: FCM 알림 기능 구현 #94

Merged
merged 2 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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