Skip to content

Commit

Permalink
Merge branch 'dev' into test
Browse files Browse the repository at this point in the history
  • Loading branch information
gouyeonch committed Oct 29, 2024
2 parents 99aa6db + aa3c557 commit 048fe42
Show file tree
Hide file tree
Showing 28 changed files with 630 additions and 89 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.data:spring-data-redis:3.0.1'

// Firebase sdk
implementation 'com.google.firebase:firebase-admin:9.2.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.kkokkomu.short_news.alarm.controller;

import com.kkokkomu.short_news.alarm.dto.request.FcmSendDto;
import com.kkokkomu.short_news.alarm.dto.request.PushAlarmDto;
import com.kkokkomu.short_news.alarm.service.FCMSendService;
import com.kkokkomu.short_news.core.annotation.UserId;
import com.kkokkomu.short_news.core.dto.ResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
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;

@Tag(name = "알람")
@RestController
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/alarm")
public class AlarmController {
private FCMSendService fcmSendService;

@Operation(summary = "알람 테스트")
@GetMapping("/test")
public ResponseDto<String> test(
@Parameter(hidden = true) @UserId Long userId,
@RequestBody PushAlarmDto pushAlarmDto
) throws IOException {
return ResponseDto.ok(fcmSendService.test(pushAlarmDto, userId));
}
}

This file was deleted.

30 changes: 13 additions & 17 deletions src/main/java/com/kkokkomu/short_news/alarm/domain/FCMToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DynamicUpdate
@Table(name = "notification_token")
@Table(name = "fcm_token")
public class FCMToken {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Expand All @@ -25,36 +24,33 @@ public class FCMToken {
@Column(name = "token", nullable = false, columnDefinition = "TINYTEXT")
private String token; // 토큰, 최대 255자

@Column(name = "modify_date", nullable = false)
private LocalDateTime mod_dtm; // 토큰 갱신일

@Column(name = "expired_date", nullable = false)
private LocalDateTime exp_dtm; // 토큰 만료일
@Column(name = "edited_at", nullable = false)
private LocalDateTime editedAt; // 토큰 갱신일

@Column(name = "device" , nullable = false)
private String device; // android or ios
@Column(name = "expired_at", nullable = false)
private LocalDateTime expiredAt; // 토큰 만료일

@Column(name = "device_id", nullable = false, unique = true)
private String deviceId;

@Builder
public FCMToken(String token, LocalDateTime mod_dtm , String device, String deviceId ){
public FCMToken(User user, String token, String deviceId ){
this.user = user;
this.token = token;
this.mod_dtm = mod_dtm;
this.exp_dtm = mod_dtm.plusMonths(1);
this.device = device;
this.editedAt = LocalDateTime.now();
this.expiredAt = LocalDateTime.now().plusMonths(1);
this.deviceId = deviceId;
}

// 토큰 갱신
public void refreshToken() {
this.mod_dtm = LocalDateTime.now();
this.exp_dtm = mod_dtm.plusMonths(1); // 1달 갱신
this.editedAt = LocalDateTime.now();
this.expiredAt = editedAt.plusMonths(1); // 1달 갱신
}

// 토큰 재생성
public void regenerateToken(FCMToken fcmToken) {
this.token = fcmToken.getToken();
public void regenerateToken(String fcmToken) {
this.token = fcmToken;
refreshToken();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.kkokkomu.short_news.alarm.dto.request;

import jakarta.validation.constraints.NotNull;

public record CreateTokenDto (
@NotNull
String fcmToken,

@NotNull
String deviceId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.kkokkomu.short_news.alarm.dto.request;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class FcmMessageDto {
private boolean validateOnly;
private FcmMessageDto.Message message;

@Builder
@AllArgsConstructor
@Getter
public static class Message {
private FcmMessageDto.Notification notification;
private String token;
}

@Builder
@AllArgsConstructor
@Getter
public static class Notification {
private String title;
private String body;
private String image;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.kkokkomu.short_news.alarm.dto.request;

import lombok.*;

@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class FcmSendDto {
private String token;

private String title;

private String body;

@Builder(toBuilder = true)
public FcmSendDto(String token, String title, String body) {
this.token = token;
this.title = title;
this.body = body;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.kkokkomu.short_news.alarm.dto.request;

import jakarta.validation.constraints.NotNull;

public record PushAlarmDto(
@NotNull String deviceId,
@NotNull String title,
@NotNull String body
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.kkokkomu.short_news.alarm.dto.response;

import com.kkokkomu.short_news.alarm.domain.FCMToken;
import lombok.Builder;

@Builder
public record FCMTokenDto(
Long id,
String token,
String deviceId,
String editedAt,
String expiredAt
) {
public static FCMTokenDto of(FCMToken fcmToken) {
return FCMTokenDto.builder()
.id(fcmToken.getId())
.token(fcmToken.getToken())
.deviceId(fcmToken.getDeviceId())
.editedAt(fcmToken.getEditedAt().toString())
.expiredAt(fcmToken.getExpiredAt().toString())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.kkokkomu.short_news.alarm.repository;

import com.kkokkomu.short_news.alarm.domain.FCMToken;
import com.kkokkomu.short_news.user.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@Repository
public interface FCMTokenRepository extends JpaRepository<FCMToken, Long> {

// 만기된 토큰들 조회
@Query("SELECT token FROM FCMToken token WHERE token.expiredAt <= :now")
List<FCMToken> findExpiredTokenList(LocalDateTime now);

List<FCMToken> findAllByDeviceId(String deviceId);

FCMToken findByToken(String token);

Optional<FCMToken> findByDeviceIdAndToken(String deviceId, String token);

FCMToken findByDeviceIdAndUserId(String deviceId, Long userId);

void deleteByDeviceIdAndUser(String deviceId, User user);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.kkokkomu.short_news.alarm.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.auth.oauth2.GoogleCredentials;
import com.kkokkomu.short_news.alarm.domain.FCMToken;
import com.kkokkomu.short_news.alarm.dto.request.FcmMessageDto;
import com.kkokkomu.short_news.alarm.dto.request.FcmSendDto;
import com.kkokkomu.short_news.alarm.dto.request.PushAlarmDto;
import com.kkokkomu.short_news.alarm.repository.FCMTokenRepository;
import com.kkokkomu.short_news.user.domain.User;
import com.kkokkomu.short_news.user.service.UserLookupService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
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.util.List;

@Service
@RequiredArgsConstructor
@Slf4j
public class FCMSendService {
@Value("${fcm.firebase-key}")
private String firebaseConfigPath;

private final FCMTokenRepository fcmTokenRepository;

private final UserLookupService userLookupService;

public String test(PushAlarmDto pushAlarmDto, Long userId) {
FCMToken fcmToken = fcmTokenRepository.findByDeviceIdAndUserId(pushAlarmDto.deviceId(), userId);

sendMessageTo(
FcmSendDto.builder()
.token(fcmToken.getToken())
.title(pushAlarmDto.title())
.body(pushAlarmDto.body())
.build()
);

return "success";
}

/**
* 푸시 메시지 처리를 수행하는 비즈니스 로직
*
* @param fcmSendDto 모바일에서 전달받은 Object
* @return 성공(1), 실패(0)
*/
public int sendMessageTo(FcmSendDto fcmSendDto) {
try {
String message = makeMessage(fcmSendDto);
RestTemplate restTemplate = new RestTemplate();
/**
* 추가된 사항 : RestTemplate 이용중 클라이언트의 한글 깨짐 증상에 대한 수정
* @refernece : https://stackoverflow.com/questions/29392422/how-can-i-tell-resttemplate-to-post-with-utf-8-encoding
*/
restTemplate.getMessageConverters()
.add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));

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/adjh54-a0189/messages:send>";
ResponseEntity response = restTemplate.exchange(API_URL, HttpMethod.POST, entity, String.class);

log.info(String.valueOf(response.getStatusCode()));

return response.getStatusCode() == HttpStatus.OK ? 1 : 0;
} catch (IOException e) {
e.printStackTrace();
return 0;
}
}

/**
* Firebase Admin SDK의 비공개 키를 참조하여 Bearer 토큰을 발급 받습니다.
*
* @return Bearer token
*/
private String getAccessToken() throws IOException {
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 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);
}
}
Loading

0 comments on commit 048fe42

Please sign in to comment.