-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ [STMT-296] 알림 토큰 갱신 API 구현 및 FCM 알림 전송 테스트 기능 구현 (#155)
* 🗃️ [STMT-296] 알림 토큰 테이블 DDL 작성 * ✨ [STMT-296] 알림 토큰 jpa entity 구현 * ✨ [STMT-296] 알림 토큰 도메인 구현 * ✨ [STMT-296] 알림 토큰 DB 저장, 쿼리 메서드 구현 * ✨ [STMT-296] 알림 토큰 갱신 유스케이스 구현 * ✨ [STMT-296] 알림 토큰 갱신 API 구현 * 🩹 [STMT-296] 알림 토큰 갱신 API 반환 HTTP 상태코드 변경 * ✅ [STMT-296] 알림 토큰 갱신 API 테스트 작성 * 📝 [STMT-296] 알림 토큰 갱신 API 명세서 작성 * ✨ [STMT-296] fcm admin 설정 * ✨ [STMT-296] fcm 알림 테스트용 알림 전송, 주제 구독 기능 구현 * 💚 [STMT-296] github CI action에 FCM account secret 환경 변수 설정 추가 * 💚 [STMT-296] FCM account secret 환경 변수 띄어쓰기 추가 * 💚 [STMT-296] firebase 디렉터리 생성 추가 * 🔧 [STMT-296] FCM account secret 경로 설정파일 수정 * 🩹 [STMT-296] resource에서 파일을 가져오고, FirebaseApp 재초기화를 방지하도록 수정 * 🩹 [STMT-296] 디버깅용 파일 내용 출력 코드 추가 * 🩹 [STMT-296] 디버깅용 출력 코드 삭제 * 🚀 [STMT-296] cicd 워크플로우 파일에 fcm account secret 환경 변수 설정 추가
- Loading branch information
Showing
31 changed files
with
593 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
src/main/java/com/stumeet/server/common/config/FirebaseAdminConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package com.stumeet.server.common.config; | ||
|
||
import java.io.IOException; | ||
import java.io.InputStream; | ||
|
||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.core.io.Resource; | ||
|
||
import com.google.auth.oauth2.GoogleCredentials; | ||
import com.google.firebase.FirebaseApp; | ||
import com.google.firebase.FirebaseOptions; | ||
|
||
@Configuration | ||
public class FirebaseAdminConfig { | ||
|
||
@Value("${fcm.secret.path}") | ||
private Resource serviceAccountResource; | ||
|
||
@Bean | ||
public FirebaseApp initializeFirebaseApp() throws IOException { | ||
if (FirebaseApp.getApps().isEmpty()) { | ||
InputStream serviceAccount = serviceAccountResource.getInputStream(); | ||
|
||
FirebaseOptions options = FirebaseOptions.builder() | ||
.setCredentials(GoogleCredentials.fromStream(serviceAccount)) | ||
.build(); | ||
|
||
return FirebaseApp.initializeApp(options); | ||
} else { | ||
return FirebaseApp.getInstance(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
...om/stumeet/server/notification/adapter/out/mapper/NotificationTokenPersistenceMapper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package com.stumeet.server.notification.adapter.out.mapper; | ||
|
||
import org.springframework.stereotype.Component; | ||
|
||
import com.stumeet.server.notification.adapter.out.persistence.NotificationTokenJpaEntity; | ||
import com.stumeet.server.notification.domain.NotificationToken; | ||
|
||
@Component | ||
public class NotificationTokenPersistenceMapper { | ||
|
||
public NotificationToken toDomain(NotificationTokenJpaEntity entity) { | ||
return NotificationToken.builder() | ||
.id(entity.getId()) | ||
.memberId(entity.getMemberId()) | ||
.deviceId(entity.getDeviceId()) | ||
.token(entity.getToken()) | ||
.build(); | ||
} | ||
|
||
public NotificationTokenJpaEntity toEntity(NotificationToken domain) { | ||
return NotificationTokenJpaEntity.builder() | ||
.id(domain.getId()) | ||
.memberId(domain.getMemberId()) | ||
.deviceId(domain.getDeviceId()) | ||
.token(domain.getToken()) | ||
.build(); | ||
} | ||
} |
69 changes: 69 additions & 0 deletions
69
...java/com/stumeet/server/notification/adapter/out/notification/FcmNotificationAdapter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package com.stumeet.server.notification.adapter.out.notification; | ||
|
||
import org.springframework.stereotype.Component; | ||
|
||
import com.google.firebase.messaging.BatchResponse; | ||
import com.google.firebase.messaging.FirebaseMessaging; | ||
import com.google.firebase.messaging.FirebaseMessagingException; | ||
import com.google.firebase.messaging.Message; | ||
import com.google.firebase.messaging.MulticastMessage; | ||
import com.google.firebase.messaging.Notification; | ||
import com.google.firebase.messaging.TopicManagementResponse; | ||
import com.stumeet.server.notification.application.port.out.ManageSubscriptionPort; | ||
import com.stumeet.server.notification.application.port.out.NotificationSendPort; | ||
import com.stumeet.server.notification.application.port.out.command.SendMessageCommand; | ||
import com.stumeet.server.notification.application.port.out.command.SubscribeCommand; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class FcmNotificationAdapter implements ManageSubscriptionPort, NotificationSendPort { | ||
|
||
@Override | ||
public void subscribe(SubscribeCommand command) throws FirebaseMessagingException { | ||
TopicManagementResponse response = FirebaseMessaging.getInstance() | ||
.subscribeToTopic(command.registrationTokens(), command.topic()); | ||
|
||
System.out.println(response.getSuccessCount() + " tokens were subscribed successfully"); | ||
} | ||
|
||
@Override | ||
public void sendTokenMulticastMessage(SendMessageCommand command) throws FirebaseMessagingException { | ||
Notification notification = makeNotification(command.title(), command.body(), command.image()); | ||
|
||
MulticastMessage multicastMessage = MulticastMessage.builder() | ||
.addAllTokens(command.registrationTokens()) | ||
.putAllData(command.data()) | ||
.setNotification(notification) | ||
.build(); | ||
|
||
BatchResponse response = FirebaseMessaging.getInstance() | ||
.sendEachForMulticast(multicastMessage); | ||
|
||
System.out.println(response.getSuccessCount() + " messages were sent successfully"); | ||
System.out.println(response); | ||
} | ||
|
||
@Override | ||
public void sendTopicMessage(SendMessageCommand command) throws FirebaseMessagingException { | ||
Notification notification = makeNotification(command.title(), command.body(), command.image()); | ||
|
||
Message message = Message.builder() | ||
.setTopic(command.topic()) | ||
.putAllData(command.data()) | ||
.setNotification(notification) | ||
.build(); | ||
|
||
String response = FirebaseMessaging.getInstance().send(message); | ||
System.out.println("Successfully sent message: " + response); | ||
} | ||
|
||
private Notification makeNotification(String title, String body, String image) { | ||
return Notification.builder() | ||
.setTitle(title) | ||
.setBody(body) | ||
.setImage(image) | ||
.build(); | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
...m/stumeet/server/notification/adapter/out/persistence/JpaNotificationTokenRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package com.stumeet.server.notification.adapter.out.persistence; | ||
|
||
import java.util.Optional; | ||
|
||
import org.springframework.data.jpa.repository.JpaRepository; | ||
|
||
public interface JpaNotificationTokenRepository extends JpaRepository<NotificationTokenJpaEntity, Long> { | ||
|
||
Optional<NotificationTokenJpaEntity> findByMemberIdAndDeviceId(Long memberId, String deviceId); | ||
} |
43 changes: 43 additions & 0 deletions
43
...a/com/stumeet/server/notification/adapter/out/persistence/NotificationTokenJpaEntity.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package com.stumeet.server.notification.adapter.out.persistence; | ||
|
||
import org.hibernate.annotations.Comment; | ||
|
||
import com.stumeet.server.common.model.BaseTimeEntity; | ||
|
||
import jakarta.persistence.Column; | ||
import jakarta.persistence.Entity; | ||
import jakarta.persistence.GeneratedValue; | ||
import jakarta.persistence.GenerationType; | ||
import jakarta.persistence.Id; | ||
import jakarta.persistence.Table; | ||
import lombok.AccessLevel; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Entity | ||
@Table(name = "notification_token") | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
@AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
@Builder | ||
@Getter | ||
public class NotificationTokenJpaEntity extends BaseTimeEntity { | ||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
@Comment("등록 토큰 ID") | ||
private Long id; | ||
|
||
@Column(name = "member_id") | ||
@Comment("멤버 ID") | ||
private Long memberId; | ||
|
||
@Column(name = "device_id", unique = true, updatable = false) | ||
@Comment("기기 식별자") | ||
private String deviceId; | ||
|
||
@Column(name = "token") | ||
@Comment("알림 토큰") | ||
private String token; | ||
} |
33 changes: 33 additions & 0 deletions
33
...meet/server/notification/adapter/out/persistence/NotificationTokenPersistenceAdapter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package com.stumeet.server.notification.adapter.out.persistence; | ||
|
||
import com.stumeet.server.common.annotation.PersistenceAdapter; | ||
import com.stumeet.server.notification.adapter.out.mapper.NotificationTokenPersistenceMapper; | ||
import com.stumeet.server.notification.domain.exception.NotExistsNotificationTokenException; | ||
import com.stumeet.server.notification.application.port.out.NotificationTokenQueryPort; | ||
import com.stumeet.server.notification.application.port.out.SaveNotificationTokenPort; | ||
import com.stumeet.server.notification.domain.NotificationToken; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
|
||
@PersistenceAdapter | ||
@RequiredArgsConstructor | ||
public class NotificationTokenPersistenceAdapter implements NotificationTokenQueryPort, SaveNotificationTokenPort { | ||
|
||
private final JpaNotificationTokenRepository jpaNotificationTokenRepository; | ||
|
||
private final NotificationTokenPersistenceMapper notificationTokenPersistenceMapper; | ||
|
||
@Override | ||
public NotificationToken findTokenForMember(Long memberId, String deviceId) { | ||
NotificationTokenJpaEntity entity = jpaNotificationTokenRepository.findByMemberIdAndDeviceId(memberId, deviceId) | ||
.orElseThrow(() -> new NotExistsNotificationTokenException(memberId)); | ||
|
||
return notificationTokenPersistenceMapper.toDomain(entity); | ||
} | ||
|
||
@Override | ||
public void save(NotificationToken notificationToken) { | ||
NotificationTokenJpaEntity entity = notificationTokenPersistenceMapper.toEntity(notificationToken); | ||
jpaNotificationTokenRepository.save(entity); | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
src/main/java/com/stumeet/server/notification/adapter/out/web/RenewNotificationTokenApi.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package com.stumeet.server.notification.adapter.out.web; | ||
|
||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
|
||
import com.stumeet.server.common.annotation.WebAdapter; | ||
import com.stumeet.server.common.auth.model.LoginMember; | ||
import com.stumeet.server.common.model.ApiResponse; | ||
import com.stumeet.server.notification.adapter.out.web.dto.RenewNotificationTokenRequest; | ||
import com.stumeet.server.notification.application.port.in.RenewNotificationTokenUseCase; | ||
import com.stumeet.server.notification.application.port.in.command.RenewNotificationTokenCommand; | ||
|
||
import jakarta.validation.Valid; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@WebAdapter | ||
@RequestMapping("/api/v1") | ||
@RequiredArgsConstructor | ||
public class RenewNotificationTokenApi { | ||
|
||
private final RenewNotificationTokenUseCase renewNotificationTokenUseCase; | ||
|
||
@PostMapping("/notification-token/renew") | ||
public ResponseEntity<ApiResponse<Void>> renewRegistrationToken( | ||
@AuthenticationPrincipal LoginMember member, | ||
@RequestBody @Valid RenewNotificationTokenRequest request | ||
) { | ||
RenewNotificationTokenCommand command = new RenewNotificationTokenCommand( | ||
member.getId(), | ||
request.deviceId(), | ||
request.notificationToken() | ||
); | ||
|
||
renewNotificationTokenUseCase.renewNotificationToken(command); | ||
|
||
return new ResponseEntity<>( | ||
ApiResponse.success(HttpStatus.CREATED.value(), "알림 토큰이 갱신되었습니다."), | ||
HttpStatus.CREATED | ||
); | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
...va/com/stumeet/server/notification/adapter/out/web/dto/RenewNotificationTokenRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.stumeet.server.notification.adapter.out.web.dto; | ||
|
||
import jakarta.validation.constraints.NotBlank; | ||
|
||
public record RenewNotificationTokenRequest( | ||
@NotBlank(message = "디바이스 식별자는 공백일 수 없습니다.") | ||
String deviceId, | ||
|
||
@NotBlank(message = "알림 토큰은 공백일 수 없습니다.") | ||
String notificationToken | ||
) { | ||
} |
7 changes: 7 additions & 0 deletions
7
...va/com/stumeet/server/notification/application/port/in/RenewNotificationTokenUseCase.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.stumeet.server.notification.application.port.in; | ||
|
||
import com.stumeet.server.notification.application.port.in.command.RenewNotificationTokenCommand; | ||
|
||
public interface RenewNotificationTokenUseCase { | ||
void renewNotificationToken(RenewNotificationTokenCommand command); | ||
} |
Oops, something went wrong.