diff --git a/src/main/java/igoMoney/BE/common/config/FCMConfig.java b/src/main/java/igoMoney/BE/common/config/FCMConfig.java index 36df9e4..4a1ee3c 100644 --- a/src/main/java/igoMoney/BE/common/config/FCMConfig.java +++ b/src/main/java/igoMoney/BE/common/config/FCMConfig.java @@ -20,7 +20,7 @@ public class FCMConfig { private String firebaseJsonFileName; @Bean - FirebaseMessaging firebaseMessaging() throws IOException { + FirebaseMessaging init() throws IOException { ClassPathResource resource = new ClassPathResource("firebase/"+firebaseJsonFileName+".json"); InputStream refreshToken = resource.getInputStream(); FirebaseApp firebaseApp = null; diff --git a/src/main/java/igoMoney/BE/common/config/RedisConfig.java b/src/main/java/igoMoney/BE/common/config/RedisConfig.java index 5e811fd..c50c8f3 100644 --- a/src/main/java/igoMoney/BE/common/config/RedisConfig.java +++ b/src/main/java/igoMoney/BE/common/config/RedisConfig.java @@ -5,6 +5,7 @@ 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 { @@ -19,4 +20,11 @@ public class RedisConfig { public RedisConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(host, port); } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + return redisTemplate; + } } diff --git a/src/main/java/igoMoney/BE/common/exception/ErrorCode.java b/src/main/java/igoMoney/BE/common/exception/ErrorCode.java index 7227554..19c5c5d 100644 --- a/src/main/java/igoMoney/BE/common/exception/ErrorCode.java +++ b/src/main/java/igoMoney/BE/common/exception/ErrorCode.java @@ -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, "로그인 요청 오류"), diff --git a/src/main/java/igoMoney/BE/common/jwt/dto/TokenDto.java b/src/main/java/igoMoney/BE/common/jwt/dto/TokenDto.java index 87c265d..1fe6ae5 100644 --- a/src/main/java/igoMoney/BE/common/jwt/dto/TokenDto.java +++ b/src/main/java/igoMoney/BE/common/jwt/dto/TokenDto.java @@ -13,4 +13,5 @@ public class TokenDto { // 서버 자체 토큰 private String refreshToken; private String provider_accessToken; private Long userId; + private String fcmToken; } diff --git a/src/main/java/igoMoney/BE/controller/AuthController.java b/src/main/java/igoMoney/BE/controller/AuthController.java index 790ff45..bc7a778 100644 --- a/src/main/java/igoMoney/BE/controller/AuthController.java +++ b/src/main/java/igoMoney/BE/controller/AuthController.java @@ -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; @@ -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 kakaoLogin(@RequestBody TokenDto accessToken) throws IOException { + public ResponseEntity 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); } @@ -75,16 +77,18 @@ public String appleLoginPage(ModelMap model) { // 2. 로그인 후 Identity Token, AuthorizationCode 받기 @PostMapping("login/apple/redirect") @ResponseBody - public ResponseEntity getAppleUserIdToken(@RequestBody FromAppleService fromAppleService) throws Exception { + public ResponseEntity getAppleUserIdToken(@RequestBody AuthAppleLoginRequest authAppleLoginRequest) throws Exception { + checkFCMToken(authAppleLoginRequest.getFCMToken()); // 3. public key 요청하기 (n, e 값 받고 키 생성) // 4. Identity Token (JWT) 검증하기 // 5. ID토큰 payload 바탕으로 회원가입 - List subNemail = appleJwtUtils.checkIdToken(fromAppleService.getId_token()); + List 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); } @@ -92,7 +96,7 @@ public ResponseEntity getAppleUserIdToken(@RequestBody FromAppleServic // [애플] 회원탈퇴 @PostMapping("signout/apple") @ResponseBody - public ResponseEntity appleSignOut(@RequestBody FromAppleService request) throws IOException { + public ResponseEntity appleSignOut(@RequestBody AuthAppleLoginRequest request) throws IOException { // 챌린지 중단 및 패배처리 challengeService.giveUpChallengeSignOut(request.getUserId()); @@ -111,6 +115,7 @@ public ResponseEntity appleSignOut(@RequestBody FromAppleService request) .token(response.getRefresh_token()) .build(); authService.appleSignOut(appleRequest); + fcmTokenService.deleteToken(request.getUserId()); return new ResponseEntity(HttpStatus.OK); } @@ -126,6 +131,7 @@ public ResponseEntity kakaoSignOut(@PathVariable Long userId){ recordService.deleteAllUserRecords(userId); // 카카오 연동해제 및 회원정보(User) 삭제 authService.kakaoSignOut(userId); + fcmTokenService.deleteToken(userId); return new ResponseEntity(HttpStatus.OK); } @@ -145,7 +151,14 @@ public ResponseEntity refreshToken(@RequestBody Map logout(@RequestBody Map 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); + } + } } diff --git a/src/main/java/igoMoney/BE/dto/request/AuthAppleLoginRequest.java b/src/main/java/igoMoney/BE/dto/request/AuthAppleLoginRequest.java index 032e98c..e8697ad 100644 --- a/src/main/java/igoMoney/BE/dto/request/AuthAppleLoginRequest.java +++ b/src/main/java/igoMoney/BE/dto/request/AuthAppleLoginRequest.java @@ -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; } diff --git a/src/main/java/igoMoney/BE/dto/request/FromAppleService.java b/src/main/java/igoMoney/BE/dto/request/FromAppleService.java deleted file mode 100644 index 4e79300..0000000 --- a/src/main/java/igoMoney/BE/dto/request/FromAppleService.java +++ /dev/null @@ -1,18 +0,0 @@ -package igoMoney.BE.dto.request; - -import lombok.*; - -@NoArgsConstructor -@AllArgsConstructor -@Builder -@Getter -@Setter -public class FromAppleService { - - private String state; - private String code; - private String id_token; - private String user; - private String refresh_token; - private Long userId; -} diff --git a/src/main/java/igoMoney/BE/service/AuthService.java b/src/main/java/igoMoney/BE/service/AuthService.java index e5e7d30..d315d07 100644 --- a/src/main/java/igoMoney/BE/service/AuthService.java +++ b/src/main/java/igoMoney/BE/service/AuthService.java @@ -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(); diff --git a/src/main/java/igoMoney/BE/service/ChallengeService.java b/src/main/java/igoMoney/BE/service/ChallengeService.java index 3948696..aa74d2d 100644 --- a/src/main/java/igoMoney/BE/service/ChallengeService.java +++ b/src/main/java/igoMoney/BE/service/ChallengeService.java @@ -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일"); // 시작 안 한 챌린지 목록 조회 @@ -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); } @@ -200,7 +200,7 @@ 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() @@ -208,7 +208,7 @@ else if (sel==1){ .title("챌린지 결과") .message(user2.getNickname()+"님! 상대방 "+ user.getNickname() +"님이 3일 연속 미출석으로 패배하셨어요.") .build(); - notificationRepository.save(notification); + notificationService.makeNotification(notification); } else if (sel==2){ Notification notification = Notification.builder() @@ -216,7 +216,7 @@ else if (sel==2){ .title("챌린지 결과") .message(user2.getNickname()+"님! 상대방 "+ user.getNickname() +"님이 신고 누적으로 패배하셨어요.") .build(); - notificationRepository.save(notification); + notificationService.makeNotification(notification); } } @@ -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); } } @@ -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(); diff --git a/src/main/java/igoMoney/BE/service/FCMTokenService.java b/src/main/java/igoMoney/BE/service/FCMTokenService.java new file mode 100644 index 0000000..376dd78 --- /dev/null +++ b/src/main/java/igoMoney/BE/service/FCMTokenService.java @@ -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); + } +} diff --git a/src/main/java/igoMoney/BE/service/NotificationService.java b/src/main/java/igoMoney/BE/service/NotificationService.java new file mode 100644 index 0000000..1b55e94 --- /dev/null +++ b/src/main/java/igoMoney/BE/service/NotificationService.java @@ -0,0 +1,24 @@ +package igoMoney.BE.service; + +import igoMoney.BE.domain.Notification; +import igoMoney.BE.repository.NotificationRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional +public class NotificationService { + + private final NotificationRepository notificationRepository; + private final FCMTokenService fcmTokenService; + + public void makeNotification(Notification notification){ + notificationRepository.save(notification); + fcmTokenService.sendNotification(notification); + } + +} diff --git a/src/main/java/igoMoney/BE/service/RecordService.java b/src/main/java/igoMoney/BE/service/RecordService.java index 4c31961..8471c66 100644 --- a/src/main/java/igoMoney/BE/service/RecordService.java +++ b/src/main/java/igoMoney/BE/service/RecordService.java @@ -32,7 +32,7 @@ public class RecordService { private final UserRepository userRepository; private final ChallengeRepository challengeRepository; private final UserReportRepository userReportRepository; - private final NotificationRepository notificationRepository; + private final NotificationService notificationService; private final ImageService imageService; private final ChallengeService challengeService; @@ -153,7 +153,7 @@ public Long reportRecord(RecordReportRequest request) { .title("챌린지 현황") .message(offender.getNickname()+"님 지출 내역은 가이드라인 위반으로 인해 삭제 되었어요. 신고 내용 확인 후 조치가 취해질 예정입니다. 신고 사유: "+reportReasons.get(request.getReason())) .build(); - notificationRepository.save(reportNotification); + notificationService.makeNotification(reportNotification); // hide record Record record = getRecordOrThrow(request.getRecordId()); @@ -168,7 +168,7 @@ public Long reportRecord(RecordReportRequest request) { .title("챌린지 결과") .message(offender.getNickname()+"님은 신고 누적으로 진행중인 챌린지는 패배처리되고 일주일 동안 챌린지 참여가 제한됩니다. 제한 해제 날짜: " + offender.getBanReleaseDate().format(dateFormat)) .build(); - notificationRepository.save(banNotification); + notificationService.makeNotification(banNotification); challengeService.cancelChallenge(offender, 2); } return userReport.getId(); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 71b47a3..b5cd9f3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,7 +16,7 @@ spring: show-sql: false generate-ddl: true hibernate: - ddl-auto: create-drop + ddl-auto: update properties: hibernate: format_sql: true diff --git a/src/test/java/igoMoney/BE/service/RedisTest.java b/src/test/java/igoMoney/BE/service/RedisTest.java new file mode 100644 index 0000000..b31effd --- /dev/null +++ b/src/test/java/igoMoney/BE/service/RedisTest.java @@ -0,0 +1,38 @@ +package igoMoney.BE.service; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.SetOperations; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class RedisTest { + + @Autowired + private RedisTemplate redisTemplate; + + @Test + public void testSet() { + // given + SetOperations setOperations = redisTemplate.opsForSet(); + String key = "setKey"; + + // when + setOperations.add(key, "h", "e", "l", "l", "o"); + + // then + Set members = setOperations.members(key); + Long size = setOperations.size(key); + + assertThat(members).containsOnly("h", "e", "l", "o"); + assertThat(size).isEqualTo(4); + } +}