From 019fda2f2a59b60df21a76d74ddad37c0e52707c Mon Sep 17 00:00:00 2001 From: yb__char <68099546+uiurihappy@users.noreply.github.com> Date: Thu, 1 Feb 2024 00:46:52 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20FCM=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#260)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: FCM Config 추가 * feat: FCM Info 추가 및 토큰 갱신, 알림 허용 여부 API 추가 * fix: spotlessApply * fix: PostConstruct javax -> jakarta * feat: Notification gitkeep 추가 * fix: 메서드 명 변경 * fix: @kdomo 리뷰 반영 * fix: description 수정 * fix: appAlarm default 값 true로 변경 * fix: return 형 void로 수정 * fix: spotlessApply * fix: throw 에러 출력 변경 * fix: 필요없는 코드 삭제 * fix: slf4j info -> error --- build.gradle | 3 ++ .../domain/member/api/MemberController.java | 16 +++++++ .../member/application/MemberService.java | 11 +++++ .../domain/member/domain/FcmInfo.java | 42 +++++++++++++++++++ .../domain/member/domain/Member.java | 14 +++++++ .../dto/request/UpdateFcmTokenRequest.java | 6 +++ .../domain/notification/api/.gitkeep | 0 .../domain/notification/application/.gitkeep | 0 .../domain/notification/dao/.gitkeep | 0 .../domain/notification/domain/.gitkeep | 0 .../domain/notification/dto/.gitkeep | 0 .../global/config/fcm/FcmConfig.java | 38 +++++++++++++++++ .../global/config/fcm/FcmService.java | 40 ++++++++++++++++++ src/main/resources/application.yml | 3 ++ 14 files changed, 173 insertions(+) create mode 100644 src/main/java/com/depromeet/domain/member/domain/FcmInfo.java create mode 100644 src/main/java/com/depromeet/domain/member/dto/request/UpdateFcmTokenRequest.java create mode 100644 src/main/java/com/depromeet/domain/notification/api/.gitkeep create mode 100644 src/main/java/com/depromeet/domain/notification/application/.gitkeep create mode 100644 src/main/java/com/depromeet/domain/notification/dao/.gitkeep create mode 100644 src/main/java/com/depromeet/domain/notification/domain/.gitkeep create mode 100644 src/main/java/com/depromeet/domain/notification/dto/.gitkeep create mode 100644 src/main/java/com/depromeet/global/config/fcm/FcmConfig.java create mode 100644 src/main/java/com/depromeet/global/config/fcm/FcmService.java diff --git a/build.gradle b/build.gradle index 29f0c22d7..797f81c8f 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,9 @@ dependencies { // AWS implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + // Firebase-admin + implementation 'com.google.firebase:firebase-admin:9.2.0' + // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' testCompileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/depromeet/domain/member/api/MemberController.java b/src/main/java/com/depromeet/domain/member/api/MemberController.java index 01dafb4fc..d86ade861 100644 --- a/src/main/java/com/depromeet/domain/member/api/MemberController.java +++ b/src/main/java/com/depromeet/domain/member/api/MemberController.java @@ -4,6 +4,7 @@ import com.depromeet.domain.member.application.MemberService; import com.depromeet.domain.member.dto.request.NicknameCheckRequest; import com.depromeet.domain.member.dto.request.NicknameUpdateRequest; +import com.depromeet.domain.member.dto.request.UpdateFcmTokenRequest; import com.depromeet.domain.member.dto.response.MemberFindOneResponse; import com.depromeet.domain.member.dto.response.MemberSearchResponse; import com.depromeet.domain.member.dto.response.MemberSocialInfoResponse; @@ -79,4 +80,19 @@ public ResponseEntity memberNicknameUpdate( memberService.updateMemberNickname(reqest); return ResponseEntity.ok().build(); } + + @Operation(summary = "토글 여부 변경", description = "기존 토글 값을 변경합니다.") + @PatchMapping("/alarm") + public ResponseEntity memberToggleAppAlarmStateUpdate() { + memberService.toggleAppAlarm(); + return ResponseEntity.ok().build(); + } + + @Operation(summary = "FCM 토큰 갱신", description = "FCM 토큰을 갱신합니다.") + @PatchMapping("/fcm-token") + public ResponseEntity memberFcmTokenUpdate( + @Valid @RequestBody UpdateFcmTokenRequest updateFcmTokenRequest) { + memberService.updateFcmToken(updateFcmTokenRequest); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/depromeet/domain/member/application/MemberService.java b/src/main/java/com/depromeet/domain/member/application/MemberService.java index dfd380d91..2b3fb6640 100644 --- a/src/main/java/com/depromeet/domain/member/application/MemberService.java +++ b/src/main/java/com/depromeet/domain/member/application/MemberService.java @@ -10,6 +10,7 @@ import com.depromeet.domain.member.domain.Profile; import com.depromeet.domain.member.dto.request.NicknameCheckRequest; import com.depromeet.domain.member.dto.request.NicknameUpdateRequest; +import com.depromeet.domain.member.dto.request.UpdateFcmTokenRequest; import com.depromeet.domain.member.dto.response.MemberFindOneResponse; import com.depromeet.domain.member.dto.response.MemberSearchResponse; import com.depromeet.domain.member.dto.response.MemberSocialInfoResponse; @@ -163,4 +164,14 @@ private ImageFileExtension getImageFileExtension(Profile profile) { } return imageFileExtension; } + + public void toggleAppAlarm() { + final Member currentMember = memberUtil.getCurrentMember(); + currentMember.toggleAppAlarmState(currentMember.getFcmInfo()); + } + + public void updateFcmToken(UpdateFcmTokenRequest updateFcmTokenRequest) { + final Member currentMember = memberUtil.getCurrentMember(); + currentMember.updateFcmToken(currentMember.getFcmInfo(), updateFcmTokenRequest.fcmToken()); + } } diff --git a/src/main/java/com/depromeet/domain/member/domain/FcmInfo.java b/src/main/java/com/depromeet/domain/member/domain/FcmInfo.java new file mode 100644 index 000000000..e34ab1bbf --- /dev/null +++ b/src/main/java/com/depromeet/domain/member/domain/FcmInfo.java @@ -0,0 +1,42 @@ +package com.depromeet.domain.member.domain; + +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class FcmInfo { + + private String fcmToken; + private Boolean appAlarm; + + @Builder(access = AccessLevel.PRIVATE) + private FcmInfo(String fcmToken, Boolean appAlarm) { + this.fcmToken = fcmToken; + this.appAlarm = appAlarm; + } + + public static FcmInfo createFcmInfo() { + return FcmInfo.builder().fcmToken("").appAlarm(true).build(); + } + + public static FcmInfo toggleAlarm(FcmInfo fcmState) { + return new FcmInfo(fcmState.getFcmToken(), !fcmState.getAppAlarm()); + } + + public static FcmInfo disableAlarm(FcmInfo fcmInfo) { + return new FcmInfo(fcmInfo.getFcmToken(), false); + } + + public static FcmInfo deleteToken(FcmInfo fcmInfo) { + return new FcmInfo("", fcmInfo.getAppAlarm()); + } + + public static FcmInfo updateToken(FcmInfo fcmState, String fcmToken) { + return new FcmInfo(fcmToken, fcmState.getAppAlarm()); + } +} diff --git a/src/main/java/com/depromeet/domain/member/domain/Member.java b/src/main/java/com/depromeet/domain/member/domain/Member.java index 4b1ab3ed3..7deedf256 100644 --- a/src/main/java/com/depromeet/domain/member/domain/Member.java +++ b/src/main/java/com/depromeet/domain/member/domain/Member.java @@ -36,6 +36,8 @@ public class Member extends BaseTimeEntity { @Embedded private OauthInfo oauthInfo; + @Embedded private FcmInfo fcmInfo; + @Enumerated(EnumType.STRING) private MemberStatus status; @@ -58,6 +60,7 @@ public class Member extends BaseTimeEntity { private Member( Profile profile, OauthInfo oauthInfo, + FcmInfo fcmInfo, MemberStatus status, MemberRole role, MemberVisibility visibility, @@ -66,6 +69,7 @@ private Member( String password) { this.profile = profile; this.oauthInfo = oauthInfo; + this.fcmInfo = fcmInfo; this.status = status; this.role = role; this.visibility = visibility; @@ -78,6 +82,7 @@ public static Member createNormalMember(OauthInfo oauthInfo, String nickname) { return Member.builder() .profile(Profile.createProfile(nickname, null)) .oauthInfo(oauthInfo) + .fcmInfo(FcmInfo.createFcmInfo()) .status(MemberStatus.NORMAL) .role(MemberRole.USER) .visibility(MemberVisibility.PUBLIC) @@ -108,6 +113,15 @@ public void withdrawal() { throw new CustomException(ErrorCode.MEMBER_ALREADY_DELETED); } this.status = MemberStatus.DELETED; + this.fcmInfo = FcmInfo.disableAlarm(FcmInfo.createFcmInfo()); + } + + public void toggleAppAlarmState(FcmInfo fcmState) { + fcmInfo = FcmInfo.toggleAlarm(fcmState); + } + + public void updateFcmToken(FcmInfo fcmState, String fcmToken) { + fcmInfo = FcmInfo.updateToken(fcmState, fcmToken); } public void updateNickname(String nickname) { diff --git a/src/main/java/com/depromeet/domain/member/dto/request/UpdateFcmTokenRequest.java b/src/main/java/com/depromeet/domain/member/dto/request/UpdateFcmTokenRequest.java new file mode 100644 index 000000000..5d2d5fe46 --- /dev/null +++ b/src/main/java/com/depromeet/domain/member/dto/request/UpdateFcmTokenRequest.java @@ -0,0 +1,6 @@ +package com.depromeet.domain.member.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record UpdateFcmTokenRequest( + @Schema(description = "FCM 토큰", defaultValue = "fcm-token-value") String fcmToken) {} diff --git a/src/main/java/com/depromeet/domain/notification/api/.gitkeep b/src/main/java/com/depromeet/domain/notification/api/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/java/com/depromeet/domain/notification/application/.gitkeep b/src/main/java/com/depromeet/domain/notification/application/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/java/com/depromeet/domain/notification/dao/.gitkeep b/src/main/java/com/depromeet/domain/notification/dao/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/java/com/depromeet/domain/notification/domain/.gitkeep b/src/main/java/com/depromeet/domain/notification/domain/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/java/com/depromeet/domain/notification/dto/.gitkeep b/src/main/java/com/depromeet/domain/notification/dto/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/java/com/depromeet/global/config/fcm/FcmConfig.java b/src/main/java/com/depromeet/global/config/fcm/FcmConfig.java new file mode 100644 index 000000000..d9010fd86 --- /dev/null +++ b/src/main/java/com/depromeet/global/config/fcm/FcmConfig.java @@ -0,0 +1,38 @@ +package com.depromeet.global.config.fcm; + +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.nio.charset.StandardCharsets; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Slf4j +public class FcmConfig { + + @Value("${fcm.certification}") + private String fcmCertification; + + @PostConstruct + public void init() { + try { + if (FirebaseApp.getApps().isEmpty()) { + FirebaseOptions options = + new FirebaseOptions.Builder() + .setCredentials( + GoogleCredentials.fromStream( + new ByteArrayInputStream( + fcmCertification.getBytes( + StandardCharsets.UTF_8)))) + .build(); + FirebaseApp.initializeApp(options); + } + } catch (Exception e) { + log.error("FCM initializing Exception: {}", e.getStackTrace()[0]); + } + } +} diff --git a/src/main/java/com/depromeet/global/config/fcm/FcmService.java b/src/main/java/com/depromeet/global/config/fcm/FcmService.java new file mode 100644 index 000000000..c1e395576 --- /dev/null +++ b/src/main/java/com/depromeet/global/config/fcm/FcmService.java @@ -0,0 +1,40 @@ +package com.depromeet.global.config.fcm; + +import com.google.api.core.ApiFuture; +import com.google.firebase.messaging.*; +import java.util.List; +import org.springframework.stereotype.Service; + +@Service +public class FcmService { + + /** + * 참고: https://firebase.google.com/support/release-notes/admin/java 위 레퍼런스에 의거하여 + * sendMulticastAsync 는 Deprecated 되어 sendEachForMulticastAsync + * + * @param tokenList: 푸시 토큰 리스트 + * @param title: 알림 제목 + * @param content: 알림 내용 + * @return ApiFuture + */ + public ApiFuture sendGroupMessageAsync( + List tokenList, String title, String content) { + MulticastMessage multicast = + MulticastMessage.builder() + .addAllTokens(tokenList) + .setNotification( + Notification.builder().setTitle(title).setBody(content).build()) + .build(); + return FirebaseMessaging.getInstance().sendEachForMulticastAsync(multicast); + } + + public ApiFuture sendMessageSync(String token, String title, String content) { + Message message = + Message.builder() + .setToken(token) + .setNotification( + Notification.builder().setTitle(title).setBody(content).build()) + .build(); + return FirebaseMessaging.getInstance().sendAsync(message); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f3604dd11..9c76efd69 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -30,3 +30,6 @@ springdoc: logging: level: com.depromeet.domain.*.api.*: debug + +fcm: + certification: ${FCM_CERTIFICATION:}