Skip to content

Commit

Permalink
Feat: FCM 디바이스 토큰 등록 API 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
yxhwxn committed Aug 19, 2024
1 parent 848eaef commit 8cfdaad
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 104 deletions.
21 changes: 15 additions & 6 deletions src/main/java/com/cmc/suppin/fcm/controller/FcmController.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.cmc.suppin.fcm.controller;

import com.cmc.suppin.fcm.controller.dto.DeviceTokenRequestDTO;
import com.cmc.suppin.fcm.controller.dto.FcmSendDTO;
import com.cmc.suppin.fcm.service.FcmService;
import com.cmc.suppin.global.response.ApiResponse;
import com.cmc.suppin.global.security.reslover.Account;
import com.cmc.suppin.global.security.reslover.CurrentAccount;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
Expand All @@ -16,22 +21,26 @@

@Slf4j
@RestController
@RequiredArgsConstructor
@Tag(name = "FCM", description = "FCM 푸시알림 관련 API")
@RequestMapping("/api/v1/fcm")
public class FcmController {

private final FcmService fcmService;

public FcmController(FcmService fcmService) {
this.fcmService = fcmService;
}

// 모바일로부터 사용자 FCM 토큰, 메시지 제목, 내용을 받아서 서비스를 처리
@PostMapping("/send")
public ResponseEntity<ApiResponse<Object>> pushMessage(@RequestBody @Validated FcmSendDTO fcmSendDto) throws IOException {
log.debug("[+] 푸시 메시지를 전송합니다. ");
log.debug("[+] 푸시 메시지를 전송합니다.");
int result = fcmService.sendMessageTo(fcmSendDto);

return ResponseEntity.ok(ApiResponse.of(result));
}

@PostMapping("/register")
@Operation(summary = "FCM 디바이스 토큰 등록 API", description = "앱을 시작할 때, 디바이스 토큰을 저장합니다.<br><br> DeviceType : ANDROID, IOS, OTHER")
public ResponseEntity<Void> registerDeviceToken(@RequestBody @Validated DeviceTokenRequestDTO request, @CurrentAccount Account account) {
fcmService.registerDeviceToken(account.userId(), request.getToken(), request.getDeviceType());
return ResponseEntity.ok().build();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.cmc.suppin.fcm.controller.dto;

import com.cmc.suppin.fcm.domain.DeviceType;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class DeviceTokenRequestDTO {
private String token;
private DeviceType deviceType; // ANDROID, IOS, OTHER
}
5 changes: 5 additions & 0 deletions src/main/java/com/cmc/suppin/fcm/domain/DeviceType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.cmc.suppin.fcm.domain;

public enum DeviceType {
ANDROID, IOS, OTHER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.cmc.suppin.fcm.domain.respository;

import com.cmc.suppin.fcm.domain.DeviceToken;
import com.cmc.suppin.member.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface DeviceTokenRepository extends JpaRepository<DeviceToken, Long> {
Optional<DeviceToken> findByDeviceToken(String deviceToken);

List<DeviceToken> findAllByMember(Member member);
}

120 changes: 118 additions & 2 deletions src/main/java/com/cmc/suppin/fcm/service/FcmService.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,128 @@
package com.cmc.suppin.fcm.service;

import com.cmc.suppin.fcm.controller.dto.FcmMessageDTO;
import com.cmc.suppin.fcm.controller.dto.FcmSendDTO;
import com.cmc.suppin.fcm.domain.DeviceToken;
import com.cmc.suppin.fcm.domain.DeviceType;
import com.cmc.suppin.fcm.domain.respository.DeviceTokenRepository;
import com.cmc.suppin.global.enums.UserStatus;
import com.cmc.suppin.member.domain.Member;
import com.cmc.suppin.member.domain.repository.MemberRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.auth.oauth2.GoogleCredentials;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.*;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;

@Service
public interface FcmService {
@Slf4j
@RequiredArgsConstructor
public class FcmService {

int sendMessageTo(FcmSendDTO fcmSendDTO) throws IOException;
private final DeviceTokenRepository deviceTokenRepository;
private final MemberRepository memberRepository;

/**
* 푸시 메시지 처리를 수행하는 비즈니스 로직
*
* @param fcmSendDTO 모바일에서 전달받은 Object
* @return 성공(1), 실패(0)
*/
public int sendMessageTo(FcmSendDTO fcmSendDTO) throws IOException {

try {
String message = makeMessage(fcmSendDTO);
RestTemplate restTemplate = new RestTemplate();

restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + getAccessToken());

HttpEntity<String> entity = new HttpEntity<>(message, headers);

String API_URL = "https://fcm.googleapis.com/v1/projects/suppin-a5657/messages:send";
ResponseEntity<String> response = restTemplate.exchange(API_URL, HttpMethod.POST, entity, String.class);

if (response.getStatusCode() == HttpStatus.OK) {
return 1;
} else {
log.error("FCM 메시지 전송 실패: {}", response.getStatusCode());
return 0;
}
} catch (Exception e) {
log.error("FCM 메시지 전송 중 예외 발생", e);
return 0;
}
}

/**
* Firebase Admin SDK의 비공개 키를 참조하여 Bearer 토큰을 발급 받습니다.
*
* @return Bearer token, String
*/
private String getAccessToken() throws IOException {
String firebaseConfigPath = "firebase/suppin-a5657-firebase-adminsdk.json";

GoogleCredentials googleCredentials = GoogleCredentials
.fromStream(new ClassPathResource(firebaseConfigPath).getInputStream())
.createScoped(List.of("<https://www.googleapis.com/auth/cloud-platform>"));

googleCredentials.refreshIfExpired();
return googleCredentials.getAccessToken().getTokenValue();
}

/**
* FCM 전송 정보를 기반으로 메시지를 구성합니다. (Object -> String)
*
* @param fcmSendDTO, 모바일에서 전달받은 Object
* @return String
*/
private String makeMessage(FcmSendDTO fcmSendDTO) throws JsonProcessingException {

ObjectMapper om = new ObjectMapper();
FcmMessageDTO fcmMessageDto = FcmMessageDTO.builder()
.message(FcmMessageDTO.Message.builder()
.token(fcmSendDTO.getToken())
.notification(FcmMessageDTO.Notification.builder()
.title(fcmSendDTO.getTitle())
.body(fcmSendDTO.getBody())
.image(null)
.build()
).build()).validateOnly(false).build();

return om.writeValueAsString(fcmMessageDto);
}

/**
* 클라이언트로부터 Device Token을 수신하여 저장합니다.
*/
@Transactional
public void registerDeviceToken(String userId, String token, DeviceType deviceType) {
Member member = memberRepository.findByUserIdAndStatusNot(userId, UserStatus.DELETED)
.orElseThrow(() -> new IllegalArgumentException("Member not found"));

deviceTokenRepository.findByDeviceToken(token)
.orElseGet(() -> {
DeviceToken deviceToken = DeviceToken.builder()
.member(member)
.deviceToken(token)
.deviceType(deviceType)
.createdAt(LocalDateTime.now())
.build();
return deviceTokenRepository.save(deviceToken);
});
}
}
96 changes: 0 additions & 96 deletions src/main/java/com/cmc/suppin/fcm/service/FcmServiceImpl.java

This file was deleted.

0 comments on commit 8cfdaad

Please sign in to comment.