Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/#52 fcm token #75

Merged
merged 9 commits into from
Feb 19, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.oeid.mogakgo.common.swagger.template;

import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

import io.oeid.mogakgo.core.properties.swagger.error.SwaggerProjectErrorExamples;
import io.oeid.mogakgo.domain.notification.presentation.dto.req.FCMTokenApiRequest;
import io.oeid.mogakgo.exception.dto.ErrorResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;

@Tag(name = "Notification", description = "μ•Œλ¦Ό κ΄€λ ¨ API")
@SuppressWarnings("unused")
public interface NotificationSwagger {

@Operation(summary = "FCM 토큰 μ €μž₯", description = "νšŒμ›μ˜ FCM 토큰을 μ €μž₯ν•  λ•Œ μ‚¬μš©ν•˜λŠ” API")
@ApiResponse(responseCode = "200", description = "FCM 토큰 μ €μž₯ 성곡")
@ApiResponse(responseCode = "404", description = "μš”μ²­ν•œ μœ μ €κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ", content = @Content(
mediaType = APPLICATION_JSON_VALUE,
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(name = "E020301", value = SwaggerProjectErrorExamples.USER_NOT_FOUND)
})
)
ResponseEntity<Void> manageFCMToken(@Parameter(hidden = true) Long userId,
FCMTokenApiRequest request);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

μ—¬κΈ°μ—” @parameter μ•ˆλ‹¬μ•„λ„ λ˜λ‚˜μš”?

}
43 changes: 43 additions & 0 deletions src/main/java/io/oeid/mogakgo/core/configuration/FCMConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.oeid.mogakgo.core.configuration;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.FirebaseMessaging;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Objects;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FCMConfig {

private FirebaseApp firebaseApp;

public FCMConfig(@Value("${firebase.key-path}") String keyPath) throws IOException {
try (InputStream credentials = getClass().getClassLoader().getResourceAsStream(keyPath)) {
List<FirebaseApp> firebaseApps = FirebaseApp.getApps();
if (firebaseApps != null && !firebaseApps.isEmpty()) {
for (FirebaseApp app : firebaseApps) {
if (app.getName().equals(FirebaseApp.DEFAULT_APP_NAME)) {
firebaseApp = app;
}
}
} else {
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(
Objects.requireNonNull(credentials)))
.build();
firebaseApp = FirebaseApp.initializeApp(options);
}
}
}

@Bean
public FirebaseMessaging firebaseMessaging() {
return FirebaseMessaging.getInstance(firebaseApp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class SwaggerProjectErrorExamples {
public static final String PROJECT_NOT_FOUND = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":404,\"code\":\"E030301\",\"message\":\"ν•΄λ‹Ή ν”„λ‘œμ νŠΈκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.\"}";
public static final String PROJECT_FORBIDDEN_OPERATION = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":403,\"code\":\"E030201\",\"message\":\"ν•΄λ‹Ή ν”„λ‘œμ νŠΈμ— λŒ€ν•œ κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€.\"}";
public static final String PROJECT_DELETION_NOT_ALLOWED = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E030106\",\"message\":\"맀칭 μ€‘μ΄κ±°λ‚˜ λŒ€κΈ°μ€‘μΈ ν”„λ‘œμ νŠΈλŠ” μ‚­μ œν•  수 μ—†μŠ΅λ‹ˆλ‹€.\"}";
public static final String USER_NOT_FOUND = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":404,\"code\":\"E020301\",\"message\":\"ν•΄λ‹Ή μœ μ €κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.\"}";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 μ™œ ν•΄λ‹Ή νŒŒμΌμ— 있죠??

private SwaggerProjectErrorExamples() {
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.oeid.mogakgo.domain.notification.application;

import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.Notification;
import io.oeid.mogakgo.domain.notification.domain.vo.FCMToken;
import io.oeid.mogakgo.domain.notification.exception.NotificationException;
import io.oeid.mogakgo.domain.notification.infrastructure.FCMTokenJpaRepository;
import io.oeid.mogakgo.domain.user.application.UserCommonService;
import io.oeid.mogakgo.exception.code.ErrorCode404;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class FCMNotificationService {

private final FCMTokenJpaRepository fcmTokenRepository;
private final UserCommonService userCommonService;
private final FirebaseMessaging firebaseMessaging;

@Transactional
public void manageToken(Long userId, String fcmToken) {
log.info("manageToken Start");
FCMToken token = fcmTokenRepository.findById(userCommonService.getUserById(userId).getId())
.orElseGet(() -> new FCMToken(userId, fcmToken));
token.updateToken(fcmToken);
fcmTokenRepository.save(token);
log.info("manageToken End");
}

public void sendNotification(Long userId, String title, String body) {
log.info("sendNotification Start");
String fcmToken = getFCMToken(userId);
// send notification
Message message = Message.builder()
.setNotification(Notification.builder()
.setTitle(title)
.setBody(body)
.build())
.setToken(fcmToken)
.build();
try {
String response = firebaseMessaging.send(message);
log.info("Successfully sent message: " + response);
} catch (FirebaseMessagingException e) {
log.error("Error sending message: " + e.getMessage());
}
log.info("sendNotification End");
}

private String getFCMToken(Long userId) {
return fcmTokenRepository.findById(userId)
.map(FCMToken::getToken)
.orElseThrow(
() -> new NotificationException(ErrorCode404.NOTIFICATION_FCM_TOKEN_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.oeid.mogakgo.domain.notification.domain.vo;

import io.oeid.mogakgo.domain.notification.exception.NotificationException;
import io.oeid.mogakgo.exception.code.ErrorCode400;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Table(name = "fcm_token_tb")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class FCMToken {

@Id
@Column(name = "id")
private Long userId;

@Column(name = "token")
private String token;

public FCMToken(Long userId, String token) {
this.userId = verifyUserId(userId);
this.token = verifyToken(token);
}

public Long verifyUserId(Long userId) {
if (userId == null) {
throw new NotificationException(ErrorCode400.USER_ID_NOT_NULL);
}
return userId;
}

public String verifyToken(String fcmToken) {
if (fcmToken == null || fcmToken.isBlank()) {
throw new NotificationException(ErrorCode400.NOTIFICATION_FCM_TOKEN_NOT_NULL);
}
return fcmToken;
}

public void updateToken(String token) {
this.token = verifyToken(token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.oeid.mogakgo.domain.notification.infrastructure;

import io.oeid.mogakgo.domain.notification.domain.vo.FCMToken;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import io.oeid.mogakgo.domain.notification.domain.Notification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface NotificationJpaRepository extends JpaRepository<Notification, Long> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.oeid.mogakgo.domain.notification.presentation;

import io.oeid.mogakgo.common.annotation.UserId;
import io.oeid.mogakgo.common.swagger.template.NotificationSwagger;
import io.oeid.mogakgo.domain.notification.application.FCMNotificationService;
import io.oeid.mogakgo.domain.notification.presentation.dto.req.FCMTokenApiRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
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;

@RestController
@RequestMapping("/api/v1/notification")
@RequiredArgsConstructor
public class NotificationController implements NotificationSwagger {

private final FCMNotificationService fcmNotificationService;

@PostMapping("/fcm")
public ResponseEntity<Void> manageFCMToken(@UserId Long userId, @RequestBody @Valid
FCMTokenApiRequest request) {
fcmNotificationService.manageToken(userId, request.getFcmToken());
return ResponseEntity.ok().build();
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.oeid.mogakgo.domain.notification.presentation.dto.req;

import jakarta.validation.constraints.NotBlank;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class FCMTokenApiRequest {

@NotBlank(message = "FCM 토큰은 ν•„μˆ˜μž…λ‹ˆλ‹€.")
private final String fcmToken;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum ErrorCode400 implements ErrorCode {

NOTIFICATION_TAG_NOT_NULL("E060001", "μ•Œλ¦Ό νƒœκ·ΈλŠ” ν•„μˆ˜κ°’μž…λ‹ˆλ‹€."),
NOTIFICATION_DETAIL_DATA_NOT_NULL("E060002", "μ•Œλ¦Ό 상세 λ°μ΄ν„°λŠ” ν•„μˆ˜κ°’μž…λ‹ˆλ‹€."),
NOTIFICATION_FCM_TOKEN_NOT_NULL("E060003", "FCM 토큰은 ν•„μˆ˜κ°’μž…λ‹ˆλ‹€."),

INVALID_PROJECT_MEETING_TIME("E030101", "ν”„λ‘œμ νŠΈ λ§Œλ‚¨ μ‹œκ°„μ΄ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."),
INVALID_PROJECT_TAG_COUNT("E030102", "ν”„λ‘œμ νŠΈ νƒœκ·Έ κ°―μˆ˜κ°€ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 1개 이상 3개 μ΄ν•˜λ‘œ μž…λ ₯ν•΄μ•Ό ν•©λ‹ˆλ‹€."),
Expand All @@ -23,6 +24,7 @@ public enum ErrorCode400 implements ErrorCode {
USER_WANTED_JOB_BAD_REQUEST("E020102", "희망 μ§λ¬΄λŠ” 3κ°œκΉŒμ§€λ§Œ 등둝 κ°€λŠ₯ν•©λ‹ˆλ‹€."),
USERNAME_SHOULD_BE_NOT_EMPTY("E020103", "μœ μ € 이름은 λΉ„μ–΄μžˆμ„ 수 μ—†μŠ΅λ‹ˆλ‹€."),
USER_REGION_SHOULD_BE_NOT_EMPTY("E020104", "μœ μ € 지역은 λΉ„μ–΄μžˆμ„ 수 μ—†μŠ΅λ‹ˆλ‹€."),
USER_ID_NOT_NULL("E020001", "μœ μ € μ•„μ΄λ””λŠ” ν•„μˆ˜κ°’μž…λ‹ˆλ‹€."),
;

private final HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
public enum ErrorCode404 implements ErrorCode {
USER_NOT_FOUND("E020301", "ν•΄λ‹Ή μœ μ €κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."),
PROJECT_NOT_FOUND("E030301", "ν•΄λ‹Ή ν”„λ‘œμ νŠΈκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."),
NOTIFICATION_FCM_TOKEN_NOT_FOUND("E060301", "ν•΄λ‹Ή μœ μ €μ˜ FCM 토큰이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."),
;

private final HttpStatus httpStatus = HttpStatus.NOT_FOUND;
Expand Down
Loading