diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f648a60..cd2a252 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -54,6 +54,14 @@ jobs: oauth2.google.token-uri: ${{ secrets.TOKEN_URI }} oauth2.google.resource-uri: ${{ secrets.RESOURCE_URI }} jwt.secret: ${{ secrets.JWT_SECRET }} + # 2-2) Create Firebase Admin SDK JSON file + - name: Create Firebase Admin SDK JSON file + env: + FIREBASE_ADMIN_SDK: ${{ secrets.FIREBASE_ADMIN_SDK }} + FIREBASE_CONFIG_PATH: /firebase/vomvom-fd09b-firebase-adminsdk-ghtjs-0070b39a4e.json + run: | + mkdir -p firebase + echo "$FIREBASE_ADMIN_SDK" > firebase/vomvom-fd09b-firebase-adminsdk-ghtjs-0070b39a4e.json # 3) gradlew 권한 설정 - name: Grant execute permission for gradlew run: chmod +x gradlew @@ -61,6 +69,8 @@ jobs: # 4) test 제외 gradle 빌드 - name: Build with Gradle run: ./gradlew clean build -x test + env: + FIREBASE_CONFIG_PATH: firebase/vomvom-fd09b-firebase-adminsdk-ghtjs-0070b39a4e.json # working-directory: ${{ env.working-directory }} # 5) AWS 인증 - name: Configure AWS credentials diff --git a/src/main/java/vom/spring/domain/member/domain/Member.java b/src/main/java/vom/spring/domain/member/domain/Member.java index 379640f..a81a659 100644 --- a/src/main/java/vom/spring/domain/member/domain/Member.java +++ b/src/main/java/vom/spring/domain/member/domain/Member.java @@ -5,6 +5,7 @@ import lombok.*; import vom.spring.domain.homepy.Homepy; import vom.spring.domain.touchpoint.Touchpoint; +import vom.spring.domain.webpush.domain.Fcm; import java.time.LocalDate; import java.util.ArrayList; @@ -39,6 +40,9 @@ public class Member { @OneToOne(mappedBy = "member", fetch = LAZY) private Homepy homepy; + @OneToOne(mappedBy = "member", fetch = LAZY) + private Fcm fcm; + public void updateNicknameAndEmailAndProfileImg(String nickname, String email, String profileImgUrl) { this.nickname = nickname; diff --git a/src/main/java/vom/spring/domain/webpush/FcmController.java b/src/main/java/vom/spring/domain/webpush/FcmController.java deleted file mode 100644 index 14daa5e..0000000 --- a/src/main/java/vom/spring/domain/webpush/FcmController.java +++ /dev/null @@ -1,37 +0,0 @@ -package vom.spring.domain.webpush; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.validation.annotation.Validated; -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 java.io.IOException; - -@Slf4j -@RestController -@RequestMapping("/api/v1/fcm") -public class FcmController { - private final FcmService fcmService; - - public FcmController(FcmService fcmService) { - this.fcmService = fcmService; - } - - @PostMapping("/send") - public ResponseEntity> pushMessage(@RequestBody @Validated FcmSendDto fcmSendDto) throws IOException, IOException { - log.debug("[+] 푸시 메시지를 전송합니다. "); - int result = fcmService.sendMessageTo(fcmSendDto); - - ApiResponseWrapper arw = ApiResponseWrapper - .builder() - .result(result) - .resultCode(SuccessCode.SELECT_SUCCESS.getStatus()) - .resultMsg(SuccessCode.SELECT_SUCCESS.getMessage()) - .build(); - return new ResponseEntity<>(arw, HttpStatus.OK); - } -} diff --git a/src/main/java/vom/spring/domain/webpush/FcmService.java b/src/main/java/vom/spring/domain/webpush/FcmService.java deleted file mode 100644 index 78637c4..0000000 --- a/src/main/java/vom/spring/domain/webpush/FcmService.java +++ /dev/null @@ -1,12 +0,0 @@ -package vom.spring.domain.webpush; - -import org.springframework.stereotype.Service; - -import java.io.IOException; - -@Service -public class FcmService { - int sendMessageTo(FcmSendDto fcmSendDto) throws IOException { - return 0; - } -} diff --git a/src/main/java/vom/spring/domain/webpush/FcmServiceImpl.java b/src/main/java/vom/spring/domain/webpush/FcmServiceImpl.java deleted file mode 100644 index 8dca721..0000000 --- a/src/main/java/vom/spring/domain/webpush/FcmServiceImpl.java +++ /dev/null @@ -1,82 +0,0 @@ -package vom.spring.domain.webpush; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.auth.oauth2.GoogleCredentials; -import org.springframework.core.io.ClassPathResource; -import org.springframework.http.*; -import org.springframework.web.client.RestTemplate; - -import java.io.IOException; -import java.util.List; - -public class FcmServiceImpl extends FcmService { - /** - * 푸시 메시지 처리를 수행하는 비즈니스 로직 - * - * @param fcmSendDto 모바일에서 전달받은 Object - * @return 성공(1), 실패(0) - */ - @Override - public int sendMessageTo(FcmSendDto fcmSendDto) throws IOException { - - String message = makeMessage(fcmSendDto); - RestTemplate restTemplate = new RestTemplate(); - - HttpHeaders headers = new HttpHeaders(); - headers.set("Content-Type", "application/json"); - headers.set("Authorization", "Bearer " + getAccessToken()); - - HttpEntity entity = new HttpEntity<>(message, headers); - - String API_URL = "https://fcm.googleapis.com/v1/projects/vomvom-fd09b/messages:send"; -// String API_URL = "https://fcm.googleapis.com/fcm/send"; - - ResponseEntity response = restTemplate.exchange(API_URL, HttpMethod.POST, entity, String.class); - - return response.getStatusCode() == HttpStatus.OK ? 1 : 0; - } - - /** - * Firebase Admin SDK의 비공개 키를 참조하여 Bearer 토큰을 발급 받습니다. - * - * playground에서 발급받은 토큰 - * - * @return Bearer token - */ - private String getAccessToken() throws IOException { - String firebaseConfigPath = "firebase/vomvom-fd09b-firebase-adminsdk-ghtjs-0070b39a4e.json"; - - GoogleCredentials googleCredentials = GoogleCredentials - .fromStream(new ClassPathResource(firebaseConfigPath).getInputStream()) - .createScoped(List.of("https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/firebase.messaging")); - - googleCredentials.refreshIfExpired(); - - System.out.println(googleCredentials.getAccessToken()); - - return googleCredentials.getAccessToken().getTokenValue(); - } - - /** - * FCM 전송 정보를 기반으로 메시지를 구성합니다. (Object -> String) - * - * @param fcmSendDto FcmSendDto - * @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); - } -} diff --git a/src/main/java/vom/spring/domain/webpush/controller/FcmController.java b/src/main/java/vom/spring/domain/webpush/controller/FcmController.java new file mode 100644 index 0000000..6ba8e8f --- /dev/null +++ b/src/main/java/vom/spring/domain/webpush/controller/FcmController.java @@ -0,0 +1,51 @@ +package vom.spring.domain.webpush.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import vom.spring.domain.webpush.dto.ApiResponseWrapper; +import vom.spring.domain.webpush.service.FcmService; +import vom.spring.domain.webpush.domain.SuccessCode; + +import java.io.IOException; + +@Slf4j +@Controller +public class FcmController { + private FcmService fcmService; + @Autowired + public FcmController(FcmService fcmService) { + this.fcmService = fcmService; + } + + @PostMapping(value = "/api/fcm/{member-id}") + public ResponseEntity setFcmToken( + @PathVariable("member-id") Long member_id, + @RequestParam String fcmToken + ) { + fcmService.setFcmToken(fcmToken, member_id); + return new ResponseEntity<>(HttpStatus.OK); + } + + // 웹 푸시 서비스 테스트용 + @PostMapping("/api/v2/fcm/send/{member_id}") + public ResponseEntity> pushMessage( + @PathVariable("member_id") Long memberId + ) throws IOException { + log.debug("[+] 푸시 메시지를 전송합니다. "); + + int result = fcmService.sendMessageTo(memberId); + + ApiResponseWrapper arw = ApiResponseWrapper + .builder() + .result(result) + .resultCode(SuccessCode.SELECT_SUCCESS.getStatus()) + .resultMsg(SuccessCode.SELECT_SUCCESS.getMessage()) + .build(); + + return new ResponseEntity<>(arw, HttpStatus.OK); + } +} diff --git a/src/main/java/vom/spring/domain/webpush/domain/Fcm.java b/src/main/java/vom/spring/domain/webpush/domain/Fcm.java new file mode 100644 index 0000000..333af7b --- /dev/null +++ b/src/main/java/vom/spring/domain/webpush/domain/Fcm.java @@ -0,0 +1,32 @@ +package vom.spring.domain.webpush.domain; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import vom.spring.domain.member.domain.Member; + +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Fcm { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @Column(nullable = false) + private String fcmToken; + + @OneToOne + @JoinColumn(referencedColumnName = "id", name = "member_id", nullable = false) + private Member member; +} diff --git a/src/main/java/vom/spring/domain/webpush/SuccessCode.java b/src/main/java/vom/spring/domain/webpush/domain/SuccessCode.java similarity index 92% rename from src/main/java/vom/spring/domain/webpush/SuccessCode.java rename to src/main/java/vom/spring/domain/webpush/domain/SuccessCode.java index 60dbdd1..d2967af 100644 --- a/src/main/java/vom/spring/domain/webpush/SuccessCode.java +++ b/src/main/java/vom/spring/domain/webpush/domain/SuccessCode.java @@ -1,4 +1,4 @@ -package vom.spring.domain.webpush; +package vom.spring.domain.webpush.domain; public enum SuccessCode { SELECT_SUCCESS(200, "데이터 조회 성공"), diff --git a/src/main/java/vom/spring/domain/webpush/ApiResponseWrapper.java b/src/main/java/vom/spring/domain/webpush/dto/ApiResponseWrapper.java similarity index 96% rename from src/main/java/vom/spring/domain/webpush/ApiResponseWrapper.java rename to src/main/java/vom/spring/domain/webpush/dto/ApiResponseWrapper.java index ce768a9..0638206 100644 --- a/src/main/java/vom/spring/domain/webpush/ApiResponseWrapper.java +++ b/src/main/java/vom/spring/domain/webpush/dto/ApiResponseWrapper.java @@ -1,4 +1,4 @@ -package vom.spring.domain.webpush; +package vom.spring.domain.webpush.dto; import lombok.Getter; diff --git a/src/main/java/vom/spring/domain/webpush/FcmMessageDto.java b/src/main/java/vom/spring/domain/webpush/dto/FcmMessageDto.java similarity index 93% rename from src/main/java/vom/spring/domain/webpush/FcmMessageDto.java rename to src/main/java/vom/spring/domain/webpush/dto/FcmMessageDto.java index ba57327..a93d0c2 100644 --- a/src/main/java/vom/spring/domain/webpush/FcmMessageDto.java +++ b/src/main/java/vom/spring/domain/webpush/dto/FcmMessageDto.java @@ -1,4 +1,4 @@ -package vom.spring.domain.webpush; +package vom.spring.domain.webpush.dto; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/vom/spring/domain/webpush/FcmSendDto.java b/src/main/java/vom/spring/domain/webpush/dto/FcmSendDto.java similarity index 90% rename from src/main/java/vom/spring/domain/webpush/FcmSendDto.java rename to src/main/java/vom/spring/domain/webpush/dto/FcmSendDto.java index 3c2fc27..f807994 100644 --- a/src/main/java/vom/spring/domain/webpush/FcmSendDto.java +++ b/src/main/java/vom/spring/domain/webpush/dto/FcmSendDto.java @@ -1,4 +1,4 @@ -package vom.spring.domain.webpush; +package vom.spring.domain.webpush.dto; import lombok.*; diff --git a/src/main/java/vom/spring/domain/webpush/repository/FcmRepository.java b/src/main/java/vom/spring/domain/webpush/repository/FcmRepository.java new file mode 100644 index 0000000..5e07305 --- /dev/null +++ b/src/main/java/vom/spring/domain/webpush/repository/FcmRepository.java @@ -0,0 +1,28 @@ +package vom.spring.domain.webpush.repository; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.PersistenceContext; +import org.springframework.stereotype.Repository; +import vom.spring.domain.webpush.domain.Fcm; + +@Repository +public class FcmRepository { + + @PersistenceContext + private EntityManager em; + + public void save(Fcm fcm) { + em.persist(fcm); + } + + public Fcm findByMember_id(Long member_id) { + try { + return em.createQuery("SELECT f FROM Fcm f WHERE f.member.id = :member_id", Fcm.class) + .setParameter("member_id", member_id) + .getSingleResult(); + } catch (NoResultException e) { + return null; + } + } +} diff --git a/src/main/java/vom/spring/domain/webpush/service/FcmService.java b/src/main/java/vom/spring/domain/webpush/service/FcmService.java new file mode 100644 index 0000000..44463bf --- /dev/null +++ b/src/main/java/vom/spring/domain/webpush/service/FcmService.java @@ -0,0 +1,134 @@ +package vom.spring.domain.webpush.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.auth.oauth2.GoogleCredentials; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import vom.spring.domain.member.domain.Member; +import vom.spring.domain.member.repository.MemberRepository; +import vom.spring.domain.webpush.dto.FcmMessageDto; +import vom.spring.domain.webpush.domain.Fcm; +import vom.spring.domain.webpush.repository.FcmRepository; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Service +public class FcmService { + private final FcmRepository fcmRepository; + private final MemberRepository memberRepository; + + @Autowired + public FcmService(FcmRepository fcmRepository, MemberRepository memberRepository) { + this.fcmRepository = fcmRepository; + this.memberRepository = memberRepository; + } + + /** + * Fcm Token 저장 + */ + @Transactional + public void setFcmToken(String fcmToken, Long member_id) { + + Member member = memberRepository.findById(member_id).get(); + + fcmRepository.save( + Fcm.builder() + .createdAt(LocalDateTime.now()) + .fcmToken(fcmToken) + .member(member) + .build() + ); + } + + /** + * 푸시 메시지 처리를 수행하는 비즈니스 로직 + * + * @return 성공(1), 실패(0) + */ + public int sendMessageTo(Long memberId) throws IOException { + + String message = makeMessage(memberId); + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer " + getAccessToken()); + + HttpEntity entity = new HttpEntity<>(message, headers); + + String API_URL = "https://fcm.googleapis.com/v1/projects/vomvom-fd09b/messages:send"; +// String API_URL = "https://fcm.googleapis.com/fcm/send"; + ResponseEntity response = null; + + try { + response = restTemplate.exchange(API_URL, HttpMethod.POST, entity, String.class); + System.out.println(response.getStatusCode()); + return response.getStatusCode() == HttpStatus.OK ? 1 : 0; + } catch (Exception e) { + log.error("[-] FCM 전송 오류 :: " + e.getMessage()); + return 0; + } + } + + /** + * Firebase Admin SDK의 비공개 키를 참조하여 Bearer 토큰을 발급 받습니다. + * + * playground에서 발급받은 토큰 + * + * @return Bearer token + */ + private String getAccessToken() throws IOException { + + String firebaseConfigPath = System.getenv("FIREBASE_CONFIG_PATH"); + if (firebaseConfigPath == null) { + firebaseConfigPath = "firebase/vomvom-fd09b-firebase-adminsdk-ghtjs-0070b39a4e.json"; + } + + GoogleCredentials googleCredentials = GoogleCredentials + .fromStream(new ClassPathResource(firebaseConfigPath).getInputStream()) + .createScoped(List.of("https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/firebase.messaging")); + + googleCredentials.refreshIfExpired(); + + System.out.println(googleCredentials.getAccessToken()); + + return googleCredentials.getAccessToken().getTokenValue(); + } + + /** + * FCM 전송 정보를 기반으로 메시지를 구성합니다. + * + * @return String + */ + private String makeMessage(Long memberId) throws JsonProcessingException { + + ObjectMapper om = new ObjectMapper(); + Fcm fcm = fcmRepository.findByMember_id(memberId); + + if (fcm == null) { + log.error("Fcm token not found for memberId: " + memberId); + throw new IllegalArgumentException("Fcm token not found for memberId: " + memberId); + } + + FcmMessageDto fcmMessageDto = FcmMessageDto.builder() + .message(FcmMessageDto.Message.builder() + .token(fcm.getFcmToken()) + .notification(FcmMessageDto.Notification.builder() + .title("테스트") + .body("테스트입니다용") + .image(null) + .build() + ).build()).validateOnly(false).build(); + + return om.writeValueAsString(fcmMessageDto); + } +}