Skip to content

Commit

Permalink
Type: FCM 푸시 알림 적용 (#42)
Browse files Browse the repository at this point in the history
[Feat] FCM push alarm
  • Loading branch information
pingowl authored Nov 11, 2023
2 parents 43dcfc4 + e308f2d commit fba330c
Show file tree
Hide file tree
Showing 17 changed files with 247 additions and 48 deletions.
9 changes: 8 additions & 1 deletion .github/workflows/github-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ jobs:
echo "${{ secrets.PROPERTIES }}" >> ./application.properties
shell: bash


# 환경 변수 설정 (Firebase)
- name: create-json
id: create-json
uses: jsdaniell/[email protected]
with:
name: ${{ secrets.FIREBASE_JSON_FILENAME }}
json: ${{ secrets.FIREBASE_JSON }}
dir: 'src/main/resources/firebase/'

- name: Build with Gradle
run: |
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ out/

### classpath ###
application.properties
p8_key.txt
p8_key.txt
firebase
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ dependencies {
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

implementation 'com.google.firebase:firebase-admin:9.1.1'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

// Querydsl 설정부
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/igoMoney/BE/common/config/FCMConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package igoMoney.BE.common.config;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.FirebaseMessaging;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

@Configuration
public class FCMConfig {

@Value("${firebase-json}")
private String firebaseJsonFileName;

@Bean
FirebaseMessaging init() throws IOException {
ClassPathResource resource = new ClassPathResource("firebase/"+firebaseJsonFileName+".json");
InputStream refreshToken = resource.getInputStream();
FirebaseApp firebaseApp = null;
List<FirebaseApp> firebaseAppList = FirebaseApp.getApps();
if(firebaseAppList != null && !firebaseAppList.isEmpty()){
for(FirebaseApp app : firebaseAppList) {
if(app.getName().equals(FirebaseApp.DEFAULT_APP_NAME)) {
firebaseApp = app;
}
}
} else {
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(refreshToken))
.build();
firebaseApp = FirebaseApp.initializeApp(options);
}
return FirebaseMessaging.getInstance(firebaseApp);
}
}
30 changes: 30 additions & 0 deletions src/main/java/igoMoney/BE/common/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package igoMoney.BE.common.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

@Configuration
public class RedisConfig {

@Value("${spring.data.redis.host}")
private String host;

@Value("${spring.data.redis.port}")
private int port;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}

@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
1 change: 1 addition & 0 deletions src/main/java/igoMoney/BE/common/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public enum ErrorCode {
ID_TOKEN_INVALID_4(HttpStatus.UNAUTHORIZED, "ID토큰이 유효하지 않습니다.(서명 검증 결과)"),
ID_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "ID토큰이 만료되었습니다."),
AUTH_CODE_INVALID(HttpStatus.UNAUTHORIZED, "Authorization Code가 유효하지 않습니다."),
FCM_TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "FCM토큰이 null입니다."),

// login 예외
LOGIN_CONNECTION_ERROR(HttpStatus.BAD_REQUEST, "로그인 요청 오류"),
Expand Down
1 change: 1 addition & 0 deletions src/main/java/igoMoney/BE/common/jwt/dto/TokenDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ public class TokenDto { // 서버 자체 토큰
private String refreshToken;
private String provider_accessToken;
private Long userId;
private String fcmToken;
}
33 changes: 23 additions & 10 deletions src/main/java/igoMoney/BE/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package igoMoney.BE.controller;

import igoMoney.BE.common.exception.CustomException;
import igoMoney.BE.common.exception.ErrorCode;
import igoMoney.BE.common.jwt.AppleJwtUtils;
import igoMoney.BE.common.jwt.dto.AppleSignOutRequest;
import igoMoney.BE.common.jwt.dto.AppleTokenResponse;
import igoMoney.BE.common.jwt.dto.TokenDto;
import igoMoney.BE.dto.request.FromAppleService;
import igoMoney.BE.dto.request.AuthAppleLoginRequest;
import igoMoney.BE.dto.response.AuthRecreateTokenResponse;
import igoMoney.BE.service.AuthService;
import igoMoney.BE.service.ChallengeService;
import igoMoney.BE.service.RecordService;
import igoMoney.BE.service.RefreshTokenService;
import igoMoney.BE.service.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
Expand All @@ -33,14 +32,17 @@ public class AuthController {
private final RecordService recordService;
private final AppleJwtUtils appleJwtUtils;
private final RefreshTokenService refreshTokenService;
private final FCMTokenService fcmTokenService;

// 카카오 로그인
@PostMapping("login/kakao")
@ResponseBody
public ResponseEntity<TokenDto> kakaoLogin(@RequestBody TokenDto accessToken) throws IOException {
public ResponseEntity<TokenDto> kakaoLogin(@RequestBody TokenDto token) throws IOException {

TokenDto response = authService.kakaoLogin(accessToken.getAccessToken());
checkFCMToken(token.getFcmToken());
TokenDto response = authService.kakaoLogin(token.getAccessToken());
refreshTokenService.saveRefreshToken(response);
fcmTokenService.saveToken(response.getUserId(), token.getFcmToken());

return ResponseEntity.status(HttpStatus.OK).body(response);
}
Expand Down Expand Up @@ -75,24 +77,26 @@ public String appleLoginPage(ModelMap model) {
// 2. 로그인 후 Identity Token, AuthorizationCode 받기
@PostMapping("login/apple/redirect")
@ResponseBody
public ResponseEntity<TokenDto> getAppleUserIdToken(@RequestBody FromAppleService fromAppleService) throws Exception {
public ResponseEntity<TokenDto> getAppleUserIdToken(@RequestBody AuthAppleLoginRequest authAppleLoginRequest) throws Exception {

checkFCMToken(authAppleLoginRequest.getFCMToken());
// 3. public key 요청하기 (n, e 값 받고 키 생성)
// 4. Identity Token (JWT) 검증하기
// 5. ID토큰 payload 바탕으로 회원가입
List<String> subNemail = appleJwtUtils.checkIdToken(fromAppleService.getId_token());
List<String> subNemail = appleJwtUtils.checkIdToken(authAppleLoginRequest.getId_token());
// DB에 data에서 받아온 정보를 가진 사용자가 있는지 조회 & 회원가입
// 6. 서버에서 직접 JWT 토큰 발급하기 (access & refresh token)
TokenDto response = authService.AppleSignUp(subNemail); // sub, email
refreshTokenService.saveRefreshToken(response);
fcmTokenService.saveToken(response.getUserId(), authAppleLoginRequest.getFCMToken());

return ResponseEntity.status(HttpStatus.OK).body(response);
}

// [애플] 회원탈퇴
@PostMapping("signout/apple")
@ResponseBody
public ResponseEntity<Void> appleSignOut(@RequestBody FromAppleService request) throws IOException {
public ResponseEntity<Void> appleSignOut(@RequestBody AuthAppleLoginRequest request) throws IOException {

// 챌린지 중단 및 패배처리
challengeService.giveUpChallengeSignOut(request.getUserId());
Expand All @@ -111,6 +115,7 @@ public ResponseEntity<Void> appleSignOut(@RequestBody FromAppleService request)
.token(response.getRefresh_token())
.build();
authService.appleSignOut(appleRequest);
fcmTokenService.deleteToken(request.getUserId());

return new ResponseEntity(HttpStatus.OK);
}
Expand All @@ -126,6 +131,7 @@ public ResponseEntity<Void> kakaoSignOut(@PathVariable Long userId){
recordService.deleteAllUserRecords(userId);
// 카카오 연동해제 및 회원정보(User) 삭제
authService.kakaoSignOut(userId);
fcmTokenService.deleteToken(userId);

return new ResponseEntity(HttpStatus.OK);
}
Expand All @@ -145,7 +151,14 @@ public ResponseEntity<AuthRecreateTokenResponse> refreshToken(@RequestBody Map<S
// public ResponseEntity<Void> logout(@RequestBody Map<String, String> accessToken) {
//
// authService.logout(accessToken.get("accessToken"));
// fcmService.deleteToken(userId);
//
// return new ResponseEntity(HttpStatus.OK);
// }

public void checkFCMToken(String fcmToken){
if(fcmToken == null){
throw new CustomException(ErrorCode.FCM_TOKEN_INVALID);
}
}
}
14 changes: 9 additions & 5 deletions src/main/java/igoMoney/BE/dto/request/AuthAppleLoginRequest.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package igoMoney.BE.dto.request;

//import jakarta.validation.constraints.NotNull;
import lombok.*;

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Setter
public class AuthAppleLoginRequest {

private String id;
private String email;
private String nickname;
private String picture ;
private String state;
private String code;
private String id_token;
private String user;
private String refresh_token;
private Long userId;
private String FCMToken;
}
18 changes: 0 additions & 18 deletions src/main/java/igoMoney/BE/dto/request/FromAppleService.java

This file was deleted.

2 changes: 1 addition & 1 deletion src/main/java/igoMoney/BE/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ public TokenDto kakaoLogin(String accessToken) throws IOException {
String id = element.getAsJsonObject().get("id").getAsString(); // 카카오 회원번호
String email = "";
String image = "";
//String nickname = "";
//String nickname = ""
email = element.getAsJsonObject().get("kakao_account").getAsJsonObject().get("email").getAsString();
image = element.getAsJsonObject().get("properties").getAsJsonObject().get("profile_image").getAsString();
//nickname = element.getAsJsonObject().get("kakao_account").getAsJsonObject().get("profile").getAsJsonObject().get("nickname").getAsString();
Expand Down
16 changes: 8 additions & 8 deletions src/main/java/igoMoney/BE/service/ChallengeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class ChallengeService {
private final UserRepository userRepository;
private final RecordRepository recordRepository;
private final ChallengeUserRepository challengeUserRepository;
private final NotificationRepository notificationRepository;
private final NotificationService notificationService;
DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("MM월 dd일");

// 시작 안 한 챌린지 목록 조회
Expand Down Expand Up @@ -148,7 +148,7 @@ public void applyChallenge(Long userId, Long challengeId) {
.title("챌린지 현황")
.message(findChallenge.getStartDate().format(dateFormat)+"부터 "+findUser.getNickname()+"님과 챌린지 시작")
.build();
notificationRepository.save(notification);
notificationService.makeNotification(notification);

}

Expand Down Expand Up @@ -200,23 +200,23 @@ public void cancelChallenge(User user, Integer sel) {
.title("챌린지 결과")
.message("상대방 "+ user.getNickname() +"님이 챌린지를 포기했어요.")
.build();
notificationRepository.save(notification);
notificationService.makeNotification(notification);
}
else if (sel==1){
Notification notification = Notification.builder()
.user(user2)
.title("챌린지 결과")
.message(user2.getNickname()+"님! 상대방 "+ user.getNickname() +"님이 3일 연속 미출석으로 패배하셨어요.")
.build();
notificationRepository.save(notification);
notificationService.makeNotification(notification);
}
else if (sel==2){
Notification notification = Notification.builder()
.user(user2)
.title("챌린지 결과")
.message(user2.getNickname()+"님! 상대방 "+ user.getNickname() +"님이 신고 누적으로 패배하셨어요.")
.build();
notificationRepository.save(notification);
notificationService.makeNotification(notification);
}
}

Expand Down Expand Up @@ -292,14 +292,14 @@ else if (((BigDecimal) obj[1]).intValue() < minCost){
.title("챌린지 결과")
.message(u.getNickname()+"님! "+lose.getNickname()+"님과의 챌린지 대결에서 승리하셔서 뱃지를 획득하게 되었어요. \uD83E\uDD47") // 🥇
.build();
notificationRepository.save(notification);
notificationService.makeNotification(notification);
} else{
Notification notification = Notification.builder()
.user(u)
.title("챌린지 결과")
.message(u.getNickname()+"님! "+findWinner.getNickname()+"님과의 챌린지 대결에서 아쉽게 승리하지 못했어요. 새로운 챌린지를 도전해보세요. \uD83D\uDE25") //😥
.build();
notificationRepository.save(notification);
notificationService.makeNotification(notification);
}

}
Expand All @@ -326,7 +326,7 @@ public void checkAttendance() {
.title("챌린지 결과")
.message(u.getNickname()+"님! 지출내역을 3일 동안 인증하지 않아서 해당 챌린지에서 패배하셨어요.")
.build();
notificationRepository.save(absentNotification);
notificationService.makeNotification(absentNotification);
if(check==1){ // 유저 둘 다 미출석
u.deleteBadge();
u.deleteBadge();
Expand Down
49 changes: 49 additions & 0 deletions src/main/java/igoMoney/BE/service/FCMTokenService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package igoMoney.BE.service;

import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.Message;
import igoMoney.BE.domain.Notification;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class FCMTokenService {

private final StringRedisTemplate tokenRedisTemplate;

public void sendNotification(Notification notification) {
Long userId = notification.getUser().getId();
if(!hasKey(userId)){ return;}
String token = getToken(userId);
Message message = Message.builder()
.putData("title", notification.getTitle())
.putData("content", notification.getMessage())
.putData("userId", String.valueOf(userId))
.setToken(token)
.build();
sendMessage(message);
}

public void saveToken(Long userId, String FCMToken){
tokenRedisTemplate.opsForValue()
.set(String.valueOf(userId), FCMToken);
}

private String getToken(Long userId) {
return tokenRedisTemplate.opsForValue().get(userId);
}

public void deleteToken(Long userId) {
tokenRedisTemplate.delete(String.valueOf(userId));
}

public boolean hasKey(Long userId){
return tokenRedisTemplate.hasKey(String.valueOf(userId));
}

private void sendMessage(Message message) {
FirebaseMessaging.getInstance().sendAsync(message);
}
}
Loading

0 comments on commit fba330c

Please sign in to comment.