From 6cd8f725fdcfd82707a9595d8990682f1e441d24 Mon Sep 17 00:00:00 2001 From: java-saeng Date: Fri, 28 Jul 2023 15:38:50 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat=20:=20restTemplate=20=EB=B9=88=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/emmsale/config/RestTemplateConfig.java | 14 ++++++++++++++ .../java/com/emmsale/login/utils/GithubClient.java | 10 ++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 backend/emm-sale/src/main/java/com/emmsale/config/RestTemplateConfig.java diff --git a/backend/emm-sale/src/main/java/com/emmsale/config/RestTemplateConfig.java b/backend/emm-sale/src/main/java/com/emmsale/config/RestTemplateConfig.java new file mode 100644 index 000000000..7215c3200 --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/config/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package com.emmsale.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/login/utils/GithubClient.java b/backend/emm-sale/src/main/java/com/emmsale/login/utils/GithubClient.java index cc0d21f44..62e194124 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/login/utils/GithubClient.java +++ b/backend/emm-sale/src/main/java/com/emmsale/login/utils/GithubClient.java @@ -5,6 +5,7 @@ import com.emmsale.login.application.dto.GithubProfileResponse; import com.emmsale.login.exception.LoginException; import com.emmsale.login.exception.LoginExceptionType; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -15,10 +16,9 @@ import org.springframework.web.client.RestTemplate; @Component +@RequiredArgsConstructor public class GithubClient { - private static final RestTemplate REST_TEMPLATE = new RestTemplate(); - @Value("${github.client.id}") private String clientId; @Value("${github.client.secret}") @@ -28,6 +28,8 @@ public class GithubClient { @Value("${github.url.profile}") private String profileUrl; + private final RestTemplate restTemplate; + public String getAccessTokenFromGithub(final String code) { final GithubAccessTokenRequest githubAccessTokenRequest = buildGithubAccessTokenRequest(code); @@ -54,7 +56,7 @@ private GithubProfileResponse getGithubProfileResponse(final String accessToken) final HttpEntity httpEntity = new HttpEntity<>(headers); - return REST_TEMPLATE + return restTemplate .exchange(profileUrl, HttpMethod.GET, httpEntity, GithubProfileResponse.class) .getBody(); } @@ -76,7 +78,7 @@ private String getAccessTokenResponse(final GithubAccessTokenRequest githubAcces headers ); - return REST_TEMPLATE + return restTemplate .exchange(accessTokenUrl, HttpMethod.POST, httpEntity, GithubAccessTokenResponse.class) .getBody() .getAccessToken(); From e4aee0352930c1912037b7b65f60b7a5961e6e7b Mon Sep 17 00:00:00 2001 From: java-saeng Date: Mon, 31 Jul 2023 12:10:57 +0900 Subject: [PATCH 2/6] =?UTF-8?q?refactor=20:=20Custom=20Exception=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=ED=9B=84=20Exception=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #133 --- .../notification/application/NotificationQueryService.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/emm-sale/src/main/java/com/emmsale/notification/application/NotificationQueryService.java b/backend/emm-sale/src/main/java/com/emmsale/notification/application/NotificationQueryService.java index 7c7b68434..a274650e6 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/notification/application/NotificationQueryService.java +++ b/backend/emm-sale/src/main/java/com/emmsale/notification/application/NotificationQueryService.java @@ -1,9 +1,11 @@ package com.emmsale.notification.application; +import static com.emmsale.notification.exception.NotificationExceptionType.NOT_FOUND_NOTIFICATION; + import com.emmsale.notification.application.dto.NotificationResponse; import com.emmsale.notification.domain.Notification; import com.emmsale.notification.domain.NotificationRepository; -import javax.persistence.EntityNotFoundException; +import com.emmsale.notification.exception.NotificationException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,9 +18,8 @@ public class NotificationQueryService { private final NotificationRepository notificationRepository; public NotificationResponse findNotificationBy(final Long notificationId) { - //TODO : Notification Exception 이 정해지면 수정 final Notification savedNotification = notificationRepository.findById(notificationId) - .orElseThrow(EntityNotFoundException::new); + .orElseThrow(() -> new NotificationException(NOT_FOUND_NOTIFICATION)); return NotificationResponse.from(savedNotification); } From 3f5229af86627698156112abb31fa019257be2dd Mon Sep 17 00:00:00 2001 From: java-saeng Date: Mon, 31 Jul 2023 12:11:18 +0900 Subject: [PATCH 3/6] =?UTF-8?q?docs=20:=20url=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=ED=9B=84=20http=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #133 --- .../emm-sale/src/main/resources/http/notification.http | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/emm-sale/src/main/resources/http/notification.http b/backend/emm-sale/src/main/resources/http/notification.http index 815c03220..ccdce57d2 100644 --- a/backend/emm-sale/src/main/resources/http/notification.http +++ b/backend/emm-sale/src/main/resources/http/notification.http @@ -1,18 +1,18 @@ ### 알림 저장 -POST http://localhost:8080/notification +POST http://localhost:8080/notifications Content-Type: application/json { "senderId" : 1, - "receiverId" : 2, - "message" : "알람이다", + "receiverId" : 3, + "message" : "테스트 알람이다", "eventId" : 1 } ### FCM 토큰 저장 API (새롭게 저장) -POST http://localhost:8080/notification/token +POST http://localhost:8080/notifications/token Content-Type: application/json { @@ -22,7 +22,7 @@ Content-Type: application/json ### FCM 토큰 저장 API (기존에 있던 것 수정) -POST http://localhost:8080/notification/token +POST http://localhost:8080/notifications/token Content-Type: application/json { From ca5218c567de265d8654edbdb0c6a5672427a5b0 Mon Sep 17 00:00:00 2001 From: java-saeng Date: Mon, 31 Jul 2023 15:16:40 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat=20:=20FCM=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=EC=97=90=20=EC=95=8C=EB=A6=BC=20=EB=B3=B4=EB=82=B4=EA=B8=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #133 --- backend/emm-sale/build.gradle | 3 + .../FirebaseCloudMessageClient.java | 121 ++++++++++++++++++ .../NotificationCommandService.java | 4 +- .../application/dto/FcmMessage.java | 33 +++++ .../exception/NotificationExceptionType.java | 16 +++ .../src/main/resources/application.yml | 4 + .../NotificationCommandServiceTest.java | 26 +++- 7 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 backend/emm-sale/src/main/java/com/emmsale/notification/application/FirebaseCloudMessageClient.java create mode 100644 backend/emm-sale/src/main/java/com/emmsale/notification/application/dto/FcmMessage.java diff --git a/backend/emm-sale/build.gradle b/backend/emm-sale/build.gradle index b5feacfc2..8b91c8f99 100644 --- a/backend/emm-sale/build.gradle +++ b/backend/emm-sale/build.gradle @@ -65,6 +65,9 @@ dependencies { // jwt implementation 'io.jsonwebtoken:jjwt:0.9.1' + + // firebase + implementation 'com.google.firebase:firebase-admin:9.2.0' } tasks.named('test') { diff --git a/backend/emm-sale/src/main/java/com/emmsale/notification/application/FirebaseCloudMessageClient.java b/backend/emm-sale/src/main/java/com/emmsale/notification/application/FirebaseCloudMessageClient.java new file mode 100644 index 000000000..8f2ee1588 --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/notification/application/FirebaseCloudMessageClient.java @@ -0,0 +1,121 @@ +package com.emmsale.notification.application; + +import static com.emmsale.notification.exception.NotificationExceptionType.*; +import static com.emmsale.notification.exception.NotificationExceptionType.CONVERTING_JSON_ERROR; +import static com.emmsale.notification.exception.NotificationExceptionType.NOT_FOUND_FCM_TOKEN; + +import com.emmsale.member.domain.Member; +import com.emmsale.member.domain.MemberRepository; +import com.emmsale.member.exception.MemberException; +import com.emmsale.member.exception.MemberExceptionType; +import com.emmsale.notification.application.dto.FcmMessage; +import com.emmsale.notification.application.dto.FcmMessage.Data; +import com.emmsale.notification.application.dto.FcmMessage.Message; +import com.emmsale.notification.domain.FcmToken; +import com.emmsale.notification.domain.FcmTokenRepository; +import com.emmsale.notification.domain.Notification; +import com.emmsale.notification.exception.NotificationException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.auth.oauth2.GoogleCredentials; +import java.io.IOException; +import java.util.List; +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.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +@RequiredArgsConstructor +@Slf4j +public class FirebaseCloudMessageClient { + + private static final String PREFIX_ACCESS_TOKEN = "Bearer "; + private static final String PREFIX_FCM_REQUEST_URL = "https://fcm.googleapis.com/v1/projects/"; + private static final String POSTFIX_FCM_REQUEST_URL = "/messages:send"; + private static final String FIREBASE_KEY_PATH = "firebase-kerdy.json"; + private static final boolean DEFAULT_VALIDATE_ONLY = false; + + private final ObjectMapper objectMapper; + private final MemberRepository memberRepository; + private final RestTemplate restTemplate; + private final FcmTokenRepository fcmTokenRepository; + + @Value("${firebase.project.id}") + private String projectId; + + public void sendMessageTo(final Long receiverId, final Notification notification) { + + final FcmToken fcmToken = fcmTokenRepository.findByMemberId(receiverId) + .orElseThrow(() -> new NotificationException(NOT_FOUND_FCM_TOKEN)); + + final String message = makeMessage(fcmToken.getToken(), notification); + + final HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + httpHeaders.add(HttpHeaders.AUTHORIZATION, PREFIX_ACCESS_TOKEN + getAccessToken()); + + final HttpEntity httpEntity = new HttpEntity<>(message, httpHeaders); + + final String fcmRequestUrl = PREFIX_FCM_REQUEST_URL + projectId + POSTFIX_FCM_REQUEST_URL; + + final ResponseEntity exchange = restTemplate.exchange( + fcmRequestUrl, + HttpMethod.POST, + httpEntity, + String.class + ); + + if (exchange.getStatusCode().isError()) { + log.error("firebase 접속 에러 = {}", exchange.getBody()); + } + } + + private String makeMessage(final String targetToken, final Notification notification) { + + final Long senderId = notification.getSenderId(); + final Member sender = memberRepository.findById(senderId) + .orElseThrow(() -> new MemberException(MemberExceptionType.NOT_FOUND_MEMBER)); + + final Data messageData = new Data( + sender.getName(), senderId.toString(), + notification.getReceiverId().toString(), notification.getMessage(), + sender.getOpenProfileUrl() + ); + + final Message message = new Message(messageData, targetToken); + + final FcmMessage fcmMessage = new FcmMessage(DEFAULT_VALIDATE_ONLY, message); + + try { + return objectMapper.writeValueAsString(fcmMessage); + } catch (JsonProcessingException e) { + log.error("메세지 보낼 때 JSON 변환 에러", e); + throw new NotificationException(CONVERTING_JSON_ERROR); + } + } + + private String getAccessToken() { + final String firebaseConfigPath = FIREBASE_KEY_PATH; + + try { + final GoogleCredentials googleCredentials = GoogleCredentials + .fromStream(new ClassPathResource(firebaseConfigPath).getInputStream()) + .createScoped(List.of("https://www.googleapis.com/auth/cloud-platform")); + + googleCredentials.refreshIfExpired(); + + return googleCredentials.getAccessToken().getTokenValue(); + } catch (IOException e) { + log.error("구글 토큰 요청 에러", e); + throw new NotificationException(GOOGLE_REQUEST_TOKEN_ERROR); + } + } +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/notification/application/NotificationCommandService.java b/backend/emm-sale/src/main/java/com/emmsale/notification/application/NotificationCommandService.java index 7368c425f..47ea22a05 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/notification/application/NotificationCommandService.java +++ b/backend/emm-sale/src/main/java/com/emmsale/notification/application/NotificationCommandService.java @@ -28,6 +28,7 @@ public class NotificationCommandService { private final NotificationRepository notificationRepository; private final FcmTokenRepository fcmTokenRepository; private final MemberRepository memberRepository; + private final FirebaseCloudMessageClient firebaseCloudMessageClient; public NotificationResponse create(final NotificationRequest notificationRequest) { final Long senderId = notificationRequest.getSenderId(); @@ -47,7 +48,8 @@ public NotificationResponse create(final NotificationRequest notificationRequest notificationRequest.getMessage() )); - //FCM에 notification 보내기 + firebaseCloudMessageClient.sendMessageTo(receiverId, savedNotification); + return NotificationResponse.from(savedNotification); } diff --git a/backend/emm-sale/src/main/java/com/emmsale/notification/application/dto/FcmMessage.java b/backend/emm-sale/src/main/java/com/emmsale/notification/application/dto/FcmMessage.java new file mode 100644 index 000000000..2228afb3c --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/notification/application/dto/FcmMessage.java @@ -0,0 +1,33 @@ +package com.emmsale.notification.application.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class FcmMessage { + + @JsonProperty("validate_only") + private final boolean validateOnly; + private final Message message; + + @RequiredArgsConstructor + @Getter + public static class Message { + + private final Data data; + private final String token; + } + + @RequiredArgsConstructor + @Getter + public static class Data { + + private final String senderName; + private final String senderId; + private final String receiverId; + private final String message; + private final String openProfileUrl; + } +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/notification/exception/NotificationExceptionType.java b/backend/emm-sale/src/main/java/com/emmsale/notification/exception/NotificationExceptionType.java index 825c27997..e842426f5 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/notification/exception/NotificationExceptionType.java +++ b/backend/emm-sale/src/main/java/com/emmsale/notification/exception/NotificationExceptionType.java @@ -13,7 +13,23 @@ public enum NotificationExceptionType implements BaseExceptionType { NOT_FOUND_NOTIFICATION( HttpStatus.NOT_FOUND, "알림이 존재하지 않습니다." + ), + + NOT_FOUND_FCM_TOKEN( + HttpStatus.NOT_FOUND, + "해당 사용자의 기기를 구별할 수 있는 FCM 토큰이 존재하지 않습니다." + ), + + CONVERTING_JSON_ERROR( + HttpStatus.INTERNAL_SERVER_ERROR, + "알림 메시지를 보낼 때 JSON으로 변환하는 과정에서 발생한 에러입니다." + ), + + GOOGLE_REQUEST_TOKEN_ERROR( + HttpStatus.INTERNAL_SERVER_ERROR, + "구글에 토큰 요청할 때 발생한 에러" ) + ; private final HttpStatus httpStatus; diff --git a/backend/emm-sale/src/main/resources/application.yml b/backend/emm-sale/src/main/resources/application.yml index 879137516..e11c6020b 100644 --- a/backend/emm-sale/src/main/resources/application.yml +++ b/backend/emm-sale/src/main/resources/application.yml @@ -38,3 +38,7 @@ security: token: secret-key: secret_key expire-length: 3_600_000_000 + +firebase: + project: + id: kerdy diff --git a/backend/emm-sale/src/test/java/com/emmsale/notification/application/NotificationCommandServiceTest.java b/backend/emm-sale/src/test/java/com/emmsale/notification/application/NotificationCommandServiceTest.java index 09ccdb30e..55d717b9d 100644 --- a/backend/emm-sale/src/test/java/com/emmsale/notification/application/NotificationCommandServiceTest.java +++ b/backend/emm-sale/src/test/java/com/emmsale/notification/application/NotificationCommandServiceTest.java @@ -5,8 +5,13 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; import com.emmsale.helper.ServiceIntegrationTestHelper; +import com.emmsale.member.domain.MemberRepository; import com.emmsale.notification.application.dto.FcmTokenRequest; import com.emmsale.notification.application.dto.NotificationModifyRequest; import com.emmsale.notification.application.dto.NotificationRequest; @@ -18,6 +23,7 @@ import com.emmsale.notification.domain.NotificationStatus; import com.emmsale.notification.exception.NotificationException; import com.emmsale.notification.exception.NotificationExceptionType; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -32,6 +38,22 @@ class NotificationCommandServiceTest extends ServiceIntegrationTestHelper { private FcmTokenRepository fcmTokenRepository; @Autowired private NotificationRepository notificationRepository; + @Autowired + private MemberRepository memberRepository; + private NotificationCommandService mockingNotificationCommandService; + private FirebaseCloudMessageClient firebaseCloudMessageClient; + + @BeforeEach + void setUp() { + firebaseCloudMessageClient = mock(FirebaseCloudMessageClient.class); + + mockingNotificationCommandService = new NotificationCommandService( + notificationRepository, + fcmTokenRepository, + memberRepository, + firebaseCloudMessageClient + ); + } @Test @DisplayName("create() : 알림을 새로 생성할 수 있다.") @@ -54,8 +76,10 @@ void test_create() throws Exception { notificationId, senderId, receiverId, message, eventId ); + doNothing().when(firebaseCloudMessageClient).sendMessageTo(anyLong(), any()); + //when - final NotificationResponse expected = notificationCommandService.create(request); + final NotificationResponse expected = mockingNotificationCommandService.create(request); //then assertThat(actual) From 25fffbb11875d48a211eb71d69630afcbacc6c74 Mon Sep 17 00:00:00 2001 From: java-saeng Date: Mon, 31 Jul 2023 15:31:22 +0900 Subject: [PATCH 5/6] =?UTF-8?q?docs=20:=20firebase=20key=EB=A5=BC=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8B=91=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/backend-dev-deploy.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/backend-dev-deploy.yml b/.github/workflows/backend-dev-deploy.yml index b34bd9fd2..126dd8388 100644 --- a/.github/workflows/backend-dev-deploy.yml +++ b/.github/workflows/backend-dev-deploy.yml @@ -28,6 +28,14 @@ jobs: - name: 백엔드 메인으로 checkout uses: actions/checkout@v3 + - name: firebase key 생성 + run: | + echo "$FIREBASE_KEY" > firebase-kerdy.json + + - name: firebase key 이동 + run: | + cp firebase-kerdy.json src/main/resources + - name: JDK 11로 설정 uses: actions/setup-java@v3 with: From 0320321dda3c9b4de2dbf1ff5f113229adc1d8a3 Mon Sep 17 00:00:00 2001 From: java-saeng Date: Mon, 31 Jul 2023 18:03:02 +0900 Subject: [PATCH 6/6] =?UTF-8?q?refactor=20:=20actual=EA=B3=BC=20expected?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #133 --- .../application/NotificationCommandServiceTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/emm-sale/src/test/java/com/emmsale/notification/application/NotificationCommandServiceTest.java b/backend/emm-sale/src/test/java/com/emmsale/notification/application/NotificationCommandServiceTest.java index 55d717b9d..d06b2506e 100644 --- a/backend/emm-sale/src/test/java/com/emmsale/notification/application/NotificationCommandServiceTest.java +++ b/backend/emm-sale/src/test/java/com/emmsale/notification/application/NotificationCommandServiceTest.java @@ -72,14 +72,14 @@ void test_create() throws Exception { eventId ); - final NotificationResponse actual = new NotificationResponse( + final NotificationResponse expected = new NotificationResponse( notificationId, senderId, receiverId, message, eventId ); doNothing().when(firebaseCloudMessageClient).sendMessageTo(anyLong(), any()); //when - final NotificationResponse expected = mockingNotificationCommandService.create(request); + final NotificationResponse actual = mockingNotificationCommandService.create(request); //then assertThat(actual)