Skip to content

Commit

Permalink
[TNT-107] feat: FCM 관련 세팅 및 어댑터 구현 (#19)
Browse files Browse the repository at this point in the history
* [TNT-107] feat: FCM 공통 adapter 구현

* [TNT-107] feat: 로그인 시 FCM 토큰 확인 및 갱신 구현

* [TNT-107] test: 테스트 코드 작성

* [TNT-107] fix: sonar

* [TNT-107] fix: sonar 문법 수정
  • Loading branch information
ymkim97 authored Jan 17, 2025
1 parent 35b025c commit 8442418
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 71 deletions.
11 changes: 8 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ sonar {
property "sonar.host.url", "https://sonarcloud.io"
property "sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacoco/test/jacocoTestReport.xml"
property "sonar.exclusions", "**/*Application*.java, **/*Config*.java, **/*GlobalExceptionHandler.java, **/Q*.java, **/DynamicQuery.java, " +
"**/*Exception.java"
"**/*Exception.java, **/*Adapter.java"
property "sonar.java.coveragePlugin", "jacoco"
}
}
Expand Down Expand Up @@ -84,7 +84,8 @@ jacocoTestReport {
fileTree(dir: it, excludes: [
"**/*Application*",
"**/*Config*",
"**/*DynamicQuery*"
"**/*DynamicQuery*",
"**/*error*"
] + Qdomains)
})
)
Expand Down Expand Up @@ -118,7 +119,8 @@ jacocoTestCoverageVerification {
'*.dto.*',
'*.*AppleEcdsaKeyProvider',
'*.Q*',
'*.DynamicQuery'
'*.DynamicQuery',
'*.*Adapter'
]
}
}
Expand Down Expand Up @@ -182,4 +184,7 @@ dependencies {
annotationProcessor("io.github.openfeign.querydsl:querydsl-apt:$queryDslVersion:jpa")
annotationProcessor("jakarta.annotation:jakarta.annotation-api")
annotationProcessor("jakarta.persistence:jakarta.persistence-api")

// Firebase
implementation 'com.google.firebase:firebase-admin:9.4.3'
}
3 changes: 3 additions & 0 deletions src/main/java/com/tnt/application/member/OAuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ public OAuthLoginResponse oauthLogin(OAuthLoginRequest request) {
return new OAuthLoginResponse(null, socialId, false);
}

findMember.updateFcmTokenIfExpired(request.fcmToken());
memberRepository.save(findMember);

String sessionId = String.valueOf(TSID.Factory.getTsid());

sessionService.createOrUpdateSession(sessionId, String.valueOf(findMember.getId()));
Expand Down
16 changes: 13 additions & 3 deletions src/main/java/com/tnt/domain/member/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public class Member extends BaseTimeEntity {
@Column(name = "social_id", nullable = false, unique = true, length = 50)
private String socialId;

@Column(name = "fcm_token", nullable = false, length = 255)
private String fcmToken;

@Column(name = "email", nullable = false, length = 100)
private String email;

Expand Down Expand Up @@ -63,11 +66,12 @@ public class Member extends BaseTimeEntity {
private SocialType socialType;

@Builder
public Member(Long id, String socialId, String email, String name, LocalDate birthday, String profileImageUrl,
boolean serviceAgreement, boolean collectionAgreement, boolean advertisementAgreement, boolean pushAgreement,
SocialType socialType) {
public Member(Long id, String socialId, String fcmToken, String email, String name, LocalDate birthday,
String profileImageUrl, boolean serviceAgreement, boolean collectionAgreement, boolean advertisementAgreement,
boolean pushAgreement, SocialType socialType) {
this.id = id;
this.socialId = socialId;
this.fcmToken = fcmToken;
this.email = email;
this.name = name;
this.birthday = birthday;
Expand All @@ -78,4 +82,10 @@ public Member(Long id, String socialId, String email, String name, LocalDate bir
this.pushAgreement = pushAgreement;
this.socialType = socialType;
}

public void updateFcmTokenIfExpired(String fcmToken) {
if (!this.fcmToken.equals(fcmToken)) {
this.fcmToken = fcmToken;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ public record OAuthLoginRequest(
@NotBlank(message = "소셜 로그인 타입은 필수입니다.")
String socialType,

@Schema(description = "FCM 토큰", example = "dsl5f7iho-28yg2g290u2fj0-23348-23r05")
@NotBlank(message = "FCM 토큰은 필수입니다.")
String fcmToken,

@Schema(description = "소셜 액세스 토큰 (카카오 로그인 시)", example = "atweroiuhoresihsgfkn", type = "string")
String socialAccessToken,

Expand Down
32 changes: 32 additions & 0 deletions src/main/java/com/tnt/global/config/FcmConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.tnt.global.config;

import java.io.FileInputStream;
import java.io.IOException;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;

@Configuration
@Profile({"prod", "dev"})
public class FcmConfig {

@Value("${firebase.config.path}")
private String firebaseConfigPath;

@Bean
public FirebaseApp firebaseApp() throws IOException {
FileInputStream serviceAccount = new FileInputStream(firebaseConfigPath);

FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.build();

return FirebaseApp.initializeApp(options);
}
}
1 change: 1 addition & 0 deletions src/main/java/com/tnt/global/error/model/ErrorMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
public enum ErrorMessage {

SERVER_ERROR("서버 에러가 발생했습니다."),
FCM_FAILED("FCM 전송에 실패했습니다."),

CLIENT_BAD_REQUEST("잘못된 요청입니다."),
FAILED_TO_PROCESS_REQUEST("요청 진행에 실패했습니다."),
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/com/tnt/infrastructure/fcm/FcmAdapter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.tnt.infrastructure.fcm;

import static com.tnt.global.error.model.ErrorMessage.*;

import org.springframework.stereotype.Component;

import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.Message;
import com.tnt.global.error.exception.TnTException;

@Component
public class FcmAdapter {

public void sendNotificationByToken(String token, String title, String body, String clickActionUrl) {
Message message = Message.builder()
.setToken(token)
.setNotification(com.google.firebase.messaging.Notification.builder()
.setTitle(title)
.setBody(body)
.build())
.putData("click_action", clickActionUrl)
.build();

try {
FirebaseMessaging.getInstance().send(message);
} catch (FirebaseMessagingException e) {
throw new TnTException(FCM_FAILED, e);
}
}
}
2 changes: 1 addition & 1 deletion src/main/resources/config
33 changes: 15 additions & 18 deletions src/test/java/com/tnt/application/member/OAuthServiceTest.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package com.tnt.application.member;

import static com.tnt.global.error.model.ErrorMessage.APPLE_AUTH_ERROR;
import static com.tnt.global.error.model.ErrorMessage.FAILED_TO_FETCH_USER_INFO;
import static com.tnt.global.error.model.ErrorMessage.UNSUPPORTED_SOCIAL_TYPE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static com.tnt.global.error.model.ErrorMessage.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.BDDMockito.*;

import java.io.IOException;
Expand Down Expand Up @@ -82,7 +79,7 @@ void tearDown() throws IOException {
@DisplayName("존재하지 않는 회원 신규 회원으로 간주하고 리턴")
void member_not_found() {
// given
OAuthLoginRequest request = new OAuthLoginRequest(KAKAO, "kakao-access-token", null, null);
OAuthLoginRequest request = new OAuthLoginRequest(KAKAO, "fcm", "kakao-access-token", null, null);

mockWebServer.enqueue(new MockResponse()
.setResponseCode(200) // 성공 응답으로 설정
Expand All @@ -105,7 +102,7 @@ void member_not_found() {
@DisplayName("지원하지 않는 소셜 타입 예외 발생")
void unsupported_social_type_error() {
// given
OAuthLoginRequest request = new OAuthLoginRequest("NAVER", "some-token", null, null);
OAuthLoginRequest request = new OAuthLoginRequest("NAVER", "fcm", "some-token", null, null);

// when & then
assertThatThrownBy(() -> oAuthService.oauthLogin(request))
Expand All @@ -117,7 +114,7 @@ void unsupported_social_type_error() {
@DisplayName("Kakao 로그인 실패 시 예외 발생")
void kakao_login_failure_error() {
// given
OAuthLoginRequest request = new OAuthLoginRequest(KAKAO, "invalid-token", null, null);
OAuthLoginRequest request = new OAuthLoginRequest(KAKAO, "fcm", "invalid-token", null, null);

String errorResponse = "{\"msg\": \"this access token does not exist\", \"code\": -401}";

Expand All @@ -136,7 +133,7 @@ void kakao_login_failure_error() {
@DisplayName("Apple 클라이언트 인증 실패")
void apple_client_authentication_failure_error() {
// given
OAuthLoginRequest request = new OAuthLoginRequest(APPLE, null, "invalid-auth-code", null);
OAuthLoginRequest request = new OAuthLoginRequest(APPLE, "fcm", null, "invalid-auth-code", null);

mockWebServer.enqueue(new MockResponse()
.setResponseCode(400)
Expand All @@ -153,7 +150,7 @@ void apple_client_authentication_failure_error() {
@DisplayName("Apple 로그인 실패 - Android")
void apple_token_verification_failure_error_android() {
// given
OAuthLoginRequest request = new OAuthLoginRequest(APPLE, "asdf", null, null);
OAuthLoginRequest request = new OAuthLoginRequest(APPLE, "fcm", "asdf", null, null);

mockWebServer.enqueue(new MockResponse().setResponseCode(401));

Expand All @@ -167,7 +164,7 @@ void apple_token_verification_failure_error_android() {
@DisplayName("Apple 로그인 실패 - iOS")
void apple_token_verification_failure_error_ios() {
// given
OAuthLoginRequest request = new OAuthLoginRequest(APPLE, null, "asdf", null);
OAuthLoginRequest request = new OAuthLoginRequest(APPLE, "fcm", null, "asdf", null);

mockWebServer.enqueue(new MockResponse().setResponseCode(400));

Expand All @@ -181,7 +178,7 @@ void apple_token_verification_failure_error_ios() {
@DisplayName("Kakao 로그인 성공")
void kakao_login_success() {
// given
OAuthLoginRequest request = new OAuthLoginRequest(KAKAO, "valid-token", null, null);
OAuthLoginRequest request = new OAuthLoginRequest(KAKAO, "fcm", "valid-token", null, null);
Member member = mock(Member.class);
given(member.getId()).willReturn(1L);

Expand Down Expand Up @@ -220,7 +217,7 @@ void apple_login_success_android() throws Exception {
.withIssuedAt(new Date())
.sign(Algorithm.RSA256(publicKey, privateKey));

OAuthLoginRequest request = new OAuthLoginRequest(APPLE, null, null, mockIdToken);
OAuthLoginRequest request = new OAuthLoginRequest(APPLE, "fcm", null, null, mockIdToken);
Member mockMember = mock(Member.class);
given(mockMember.getId()).willReturn(1L);

Expand Down Expand Up @@ -277,7 +274,7 @@ void apple_login_success_ios() throws Exception {
.withIssuedAt(new Date())
.sign(Algorithm.RSA256(rsaPublicKey, rsaPrivateKey));

OAuthLoginRequest request = new OAuthLoginRequest(APPLE, null, mockAuthCode, null);
OAuthLoginRequest request = new OAuthLoginRequest(APPLE, "fcm", null, mockAuthCode, null);
Member mockMember = mock(Member.class);
given(mockMember.getId()).willReturn(1L);

Expand Down Expand Up @@ -324,7 +321,7 @@ void no_matching_key_error() {
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.setBody("{\"keys\": [{\"kid\": \"different-kid\"}]}"));

OAuthLoginRequest request = new OAuthLoginRequest(APPLE, null, null, mockIdToken);
OAuthLoginRequest request = new OAuthLoginRequest(APPLE, "fcm", null, null, mockIdToken);

// when & then
assertThatThrownBy(() -> oAuthService.oauthLogin(request))
Expand All @@ -336,7 +333,7 @@ void no_matching_key_error() {
@DisplayName("다양한 HTTP 상태코드에 따른 에러 처리")
void handle_error_response_with_different_status() {
// given
OAuthLoginRequest request = new OAuthLoginRequest(KAKAO, "invalid-token", null, null);
OAuthLoginRequest request = new OAuthLoginRequest(KAKAO, "fcm", "invalid-token", null, null);

// 401 Unauthorized
mockWebServer.enqueue(new MockResponse()
Expand All @@ -363,7 +360,7 @@ void fetch_apple_public_keys_error() {
.setResponseCode(500) // JWKS 엔드포인트 실패
.setBody("Server Error"));

OAuthLoginRequest request = new OAuthLoginRequest(APPLE, null, null, validToken);
OAuthLoginRequest request = new OAuthLoginRequest(APPLE, "fcm", null, null, validToken);

// when & then
assertThatThrownBy(() -> oAuthService.oauthLogin(request))
Expand All @@ -385,7 +382,7 @@ void invalid_jwks_format_error() {
.setResponseCode(200)
.setBody("invalid json")); // 잘못된 JSON 형식

OAuthLoginRequest request = new OAuthLoginRequest(APPLE, null, null, validToken);
OAuthLoginRequest request = new OAuthLoginRequest(APPLE, "fcm", null, null, validToken);

// when & then
assertThatThrownBy(() -> oAuthService.oauthLogin(request))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.tnt.domain.member;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.*;

import java.time.LocalDate;

Expand Down
31 changes: 29 additions & 2 deletions src/test/java/com/tnt/domain/member/MemberTest.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.tnt.domain.member;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;
import static org.assertj.core.api.Assertions.*;

import java.time.Instant;
import java.time.LocalDate;
Expand Down Expand Up @@ -102,5 +101,33 @@ void verify_tsid_timestamp_success() {
// then
assertThat(timestamp).isCloseTo(Instant.now(), within(1, ChronoUnit.SECONDS));
}

@Test
@DisplayName("FCM 토큰 갱신 성공")
void update_fcm_token_success() {
// given
Member member = Member.builder()
.id(TSID.fast().toLong()) // TSID 직접 생성
.socialId("12345")
.fcmToken("old-fcm-token")
.email("[email protected]")
.name("홍길동")
.birthday(LocalDate.parse("2022-01-01"))
.profileImageUrl("http://example.com")
.serviceAgreement(true)
.collectionAgreement(true)
.advertisementAgreement(true)
.pushAgreement(true)
.socialType(SocialType.KAKAO)
.build();

String newFcmToken = "new-fcm-token";

// when
member.updateFcmTokenIfExpired(newFcmToken);

// then
assertThat(member.getFcmToken()).isEqualTo(newFcmToken);
}
}
}
Loading

0 comments on commit 8442418

Please sign in to comment.