diff --git a/.github/workflows/backend-dev-ci-cd.yml b/.github/workflows/backend-dev-ci-cd.yml index 6a7dc9a4b..1f79658b3 100644 --- a/.github/workflows/backend-dev-ci-cd.yml +++ b/.github/workflows/backend-dev-ci-cd.yml @@ -7,12 +7,12 @@ on: - "backend/**" - ".github/workflows/backend-dev-ci-cd.yml" - "Dockerfile" -# pull_request: -# branches: [ "develop" ] -# paths: -# - "backend/**" -# - ".github/workflows/backend-dev-ci-cd.yml" -# - "Dockerfile" + # pull_request: + # branches: [ "develop" ] + # paths: + # - "backend/**" + # - ".github/workflows/backend-dev-ci-cd.yml" + # - "Dockerfile" jobs: @@ -44,6 +44,10 @@ jobs: - name: Set Application yml for dev run: | echo "${{ secrets.APPLICATION_PROPERTIES_DEV }}" > src/main/resources/application.properties + mkdir -p src/main/resources/fcm + echo '${{ secrets.FCM_SECRET_KEY }}' > src/main/resources/fcm/chongdaemarket-fcm-key.json + mkdir -p src/test/resources/fcm + echo '${{ secrets.FCM_SECRET_KEY }}' > src/test/resources/fcm/chongdaemarket-fcm-key.json working-directory: ./backend - name: Build with Gradle Wrapper diff --git a/.github/workflows/backend-prod-ci-cd.yml b/.github/workflows/backend-prod-ci-cd.yml index 8c605ad52..10f6faacb 100644 --- a/.github/workflows/backend-prod-ci-cd.yml +++ b/.github/workflows/backend-prod-ci-cd.yml @@ -44,6 +44,10 @@ jobs: - name: Set Application yml for prod run: | echo "${{ secrets.APPLICATION_PROPERTIES_PROD }}" > src/main/resources/application.properties + mkdir -p src/main/resources/fcm + echo '${{ secrets.FCM_SECRET_KEY }}' > src/main/resources/fcm/chongdaemarket-fcm-key.json + mkdir -p src/test/resources/fcm + echo '${{ secrets.FCM_SECRET_KEY }}' > src/test/resources/fcm/chongdaemarket-fcm-key.json working-directory: ./backend - name: Build with Gradle Wrapper diff --git a/backend/.gitignore b/backend/.gitignore index 86aaf3f4b..9731aa956 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -46,4 +46,4 @@ openapi3.yaml .idea ### FCM ### -/src/main/resources/fcm \ No newline at end of file +/src/main/resources/fcm diff --git a/backend/src/main/java/com/zzang/chongdae/auth/controller/AuthController.java b/backend/src/main/java/com/zzang/chongdae/auth/controller/AuthController.java index b454b7693..c8121bfd3 100644 --- a/backend/src/main/java/com/zzang/chongdae/auth/controller/AuthController.java +++ b/backend/src/main/java/com/zzang/chongdae/auth/controller/AuthController.java @@ -9,7 +9,6 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -28,7 +27,7 @@ public class AuthController { @LoggingMasked @PostMapping("/auth/login/kakao") public ResponseEntity kakaoLogin( - @RequestBody @Valid KakaoLoginRequest request, HttpServletResponse servletResponse) { + @RequestBody KakaoLoginRequest request, HttpServletResponse servletResponse) { AuthInfoDto authInfo = authService.kakaoLogin(request); addTokenToHttpServletResponse(authInfo.authToken(), servletResponse); LoginResponse response = new LoginResponse(authInfo.authMember()); diff --git a/backend/src/main/java/com/zzang/chongdae/auth/service/AuthService.java b/backend/src/main/java/com/zzang/chongdae/auth/service/AuthService.java index 5d6b14e55..43c21a42b 100644 --- a/backend/src/main/java/com/zzang/chongdae/auth/service/AuthService.java +++ b/backend/src/main/java/com/zzang/chongdae/auth/service/AuthService.java @@ -11,14 +11,19 @@ import com.zzang.chongdae.member.repository.MemberRepository; import com.zzang.chongdae.member.repository.entity.MemberEntity; import com.zzang.chongdae.member.service.NicknameGenerator; +import com.zzang.chongdae.notification.service.FcmNotificationService; import java.util.UUID; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +@Slf4j @RequiredArgsConstructor @Service public class AuthService { + private final FcmNotificationService notificationService; private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; private final JwtTokenProvider jwtTokenProvider; @@ -26,26 +31,47 @@ public class AuthService { private final AuthClient authClient; @WriterDatabase + @Transactional public AuthInfoDto kakaoLogin(KakaoLoginRequest request) { String loginId = authClient.getKakaoUserInfo(request.accessToken()); AuthProvider provider = AuthProvider.KAKAO; MemberEntity member = memberRepository.findByLoginId(loginId) .orElseGet(() -> signup(provider, loginId, request.fcmToken())); - return login(member); + return login(member, request.fcmToken()); } private MemberEntity signup(AuthProvider provider, String loginId, String fcmToken) { String password = passwordEncoder.encode(UUID.randomUUID().toString()); - MemberEntity member = new MemberEntity(nickNameGenerator.generate(), provider, loginId, password, fcmToken); + MemberEntity member = createMember(provider, loginId, fcmToken, password); return memberRepository.save(member); } - private AuthInfoDto login(MemberEntity member) { + private MemberEntity createMember(AuthProvider provider, String loginId, String fcmToken, String password) { + if (fcmToken == null) { + return new MemberEntity(nickNameGenerator.generate(), provider, loginId, password); + } + return new MemberEntity(nickNameGenerator.generate(), provider, loginId, password, fcmToken); + } + + private AuthInfoDto login(MemberEntity member, String fcmToken) { AuthMemberDto authMember = new AuthMemberDto(member); AuthTokenDto authToken = jwtTokenProvider.createAuthToken(member.getId().toString()); + checkFcmToken(member, fcmToken); + notificationService.login(member); return new AuthInfoDto(authMember, authToken); } + private void checkFcmToken(MemberEntity member, String fcmToken) { + if (fcmToken == null) { + member.updateFcmTokenDefault(); + return; + } + if (!memberRepository.existsByIdAndFcmToken(member.getId(), fcmToken)) { + log.info("토큰 갱신 사용자 id: {}", member.getId()); + member.updateFcmToken(fcmToken); + } + } + public AuthTokenDto refresh(String refreshToken) { Long memberId = jwtTokenProvider.getMemberIdByRefreshToken(refreshToken); return jwtTokenProvider.createAuthToken(memberId.toString()); diff --git a/backend/src/main/java/com/zzang/chongdae/auth/service/dto/KakaoLoginRequest.java b/backend/src/main/java/com/zzang/chongdae/auth/service/dto/KakaoLoginRequest.java index 5dee0e329..d775de281 100644 --- a/backend/src/main/java/com/zzang/chongdae/auth/service/dto/KakaoLoginRequest.java +++ b/backend/src/main/java/com/zzang/chongdae/auth/service/dto/KakaoLoginRequest.java @@ -1,9 +1,5 @@ package com.zzang.chongdae.auth.service.dto; -import javax.annotation.Nullable; - public record KakaoLoginRequest(String accessToken, - - @Nullable String fcmToken) { } diff --git a/backend/src/main/java/com/zzang/chongdae/comment/service/CommentService.java b/backend/src/main/java/com/zzang/chongdae/comment/service/CommentService.java index 750269466..aad4df9e7 100644 --- a/backend/src/main/java/com/zzang/chongdae/comment/service/CommentService.java +++ b/backend/src/main/java/com/zzang/chongdae/comment/service/CommentService.java @@ -46,8 +46,8 @@ public Long saveComment(CommentSaveRequest request, MemberEntity member) { CommentEntity comment = new CommentEntity(member, offering, request.content()); CommentEntity savedComment = commentRepository.save(comment); - List members = offeringMemberRepository.findAllByOffering(offering); - notificationService.saveComment(savedComment, members); + List offeringMembers = offeringMemberRepository.findAllByOffering(offering); + notificationService.saveComment(savedComment, offeringMembers); return savedComment.getId(); } diff --git a/backend/src/main/java/com/zzang/chongdae/member/repository/MemberRepository.java b/backend/src/main/java/com/zzang/chongdae/member/repository/MemberRepository.java index d2bf2ec0d..678f4ddfb 100644 --- a/backend/src/main/java/com/zzang/chongdae/member/repository/MemberRepository.java +++ b/backend/src/main/java/com/zzang/chongdae/member/repository/MemberRepository.java @@ -13,4 +13,6 @@ public interface MemberRepository extends JpaRepository { boolean existsByNickname(String nickname); Optional findByLoginId(String loginId); + + boolean existsByIdAndFcmToken(Long id, String fcmToken); } diff --git a/backend/src/main/java/com/zzang/chongdae/member/repository/entity/MemberEntity.java b/backend/src/main/java/com/zzang/chongdae/member/repository/entity/MemberEntity.java index 7e213b7c1..4e3674bec 100644 --- a/backend/src/main/java/com/zzang/chongdae/member/repository/entity/MemberEntity.java +++ b/backend/src/main/java/com/zzang/chongdae/member/repository/entity/MemberEntity.java @@ -11,6 +11,7 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; +import java.util.UUID; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; @@ -25,6 +26,8 @@ @Entity public class MemberEntity extends BaseTimeEntity { + private static final String DEFAULT_FCM_TOKEN = "invalid"; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -51,7 +54,19 @@ public MemberEntity(String nickname, AuthProvider provider, String loginId, Stri this(null, nickname, provider, loginId, password, fcmToken); } + public MemberEntity(String nickname, AuthProvider provider, String loginId, String password) { + this(null, nickname, provider, loginId, password, DEFAULT_FCM_TOKEN + UUID.randomUUID()); + } + public boolean isSame(MemberEntity other) { return this.equals(other); } + + public void updateFcmToken(String fcmToken) { + this.fcmToken = fcmToken; + } + + public void updateFcmTokenDefault() { + this.fcmToken = DEFAULT_FCM_TOKEN + UUID.randomUUID(); + } } diff --git a/backend/src/main/java/com/zzang/chongdae/notification/config/FcmConfig.java b/backend/src/main/java/com/zzang/chongdae/notification/config/FcmConfig.java index 6d42f0509..c8341ec7a 100644 --- a/backend/src/main/java/com/zzang/chongdae/notification/config/FcmConfig.java +++ b/backend/src/main/java/com/zzang/chongdae/notification/config/FcmConfig.java @@ -3,13 +3,8 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; -import com.zzang.chongdae.global.exception.MarketException; -import com.zzang.chongdae.notification.exception.NotificationErrorCode; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URL; +import java.io.InputStream; import javax.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -29,23 +24,16 @@ public void initialize() { return; } try { - URL url = this.getClass().getResource(secretKeyPath); - if (url == null) { - throw new MarketException(NotificationErrorCode.CANNOT_FIND_URL); - } - File secretKeyFile = new File(url.toURI()); - FileInputStream secretKey = new FileInputStream(secretKeyFile); + InputStream secretKey = this.getClass().getResourceAsStream(secretKeyPath); FirebaseApp.initializeApp(fcmOptions(secretKey)); log.info("성공적으로 FCM 앱을 초기화하였습니다."); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); - } catch (URISyntaxException e) { // todo - throw new RuntimeException(e); } } - private FirebaseOptions fcmOptions(FileInputStream secretKey) throws IOException { + private FirebaseOptions fcmOptions(InputStream secretKey) throws IOException { return FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(secretKey)) .build(); diff --git a/backend/src/main/java/com/zzang/chongdae/notification/domain/CommentNotification.java b/backend/src/main/java/com/zzang/chongdae/notification/domain/CommentNotification.java deleted file mode 100644 index ca391c58a..000000000 --- a/backend/src/main/java/com/zzang/chongdae/notification/domain/CommentNotification.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.zzang.chongdae.notification.domain; - -import com.google.firebase.messaging.MulticastMessage; -import com.zzang.chongdae.comment.repository.entity.CommentEntity; -import com.zzang.chongdae.member.repository.entity.MemberEntity; -import com.zzang.chongdae.notification.service.FcmMessageManager; -import com.zzang.chongdae.offeringmember.repository.entity.OfferingMemberEntity; -import java.util.List; -import javax.annotation.Nullable; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class CommentNotification { - - private final FcmMessageManager messageManager; - private final CommentEntity comment; - private final List members; - - public CommentNotification(FcmMessageManager messageManager, CommentEntity comment, - List members) { - this.messageManager = messageManager; - this.comment = comment; - this.members = members.stream() - .map(OfferingMemberEntity::getMember) - .toList(); - } - - @Nullable - public MulticastMessage messageWhenSaveComment() { - FcmTokens tokens = FcmTokens.from(membersNotWriter()); - if (tokens.isEmpty()) { - return null; - } - return messageManager.createMessages( - tokens, - comment.getOffering().getTitle(), - "%s: %s".formatted(comment.getMember().getNickname(), comment.getContent())); - } - - private List membersNotWriter() { - return members.stream() - .filter(member -> !member.isSame(comment.getMember())) - .toList(); - } -} diff --git a/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmCondition.java b/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmCondition.java index 9388c341a..883046d71 100644 --- a/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmCondition.java +++ b/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmCondition.java @@ -1,5 +1,6 @@ package com.zzang.chongdae.notification.domain; +import com.zzang.chongdae.offering.repository.entity.OfferingEntity; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -7,5 +8,14 @@ @RequiredArgsConstructor public class FcmCondition { + private static final String CONDITION_FORMAT_TRUE_AND_FALSE = "'%s' in topics && !('%s' in topics)"; + private final String value; + + public static FcmCondition offeringCondition(OfferingEntity offering) { + FcmTopic memberTopic = FcmTopic.memberTopic(); + FcmTopic proposerTopic = FcmTopic.proposerTopic(offering); + String value = CONDITION_FORMAT_TRUE_AND_FALSE.formatted(memberTopic.getValue(), proposerTopic.getValue()); + return new FcmCondition(value); + } } diff --git a/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmData.java b/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmData.java new file mode 100644 index 000000000..98ae50a9c --- /dev/null +++ b/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmData.java @@ -0,0 +1,27 @@ +package com.zzang.chongdae.notification.domain; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class FcmData { + + private final Map data = new HashMap<>(); + + public void addData(String key, Object value) { + data.put(key, value.toString()); + } + + public Map getData() { + data.forEach(this::logWithoutBody); + return Collections.unmodifiableMap(data); + } + + private void logWithoutBody(String key, String value) { + if (!key.equals("body")) { + log.info("{} : {}", key, value); + } + } +} diff --git a/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmToken.java b/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmToken.java index c8b5252cf..f2e2936c3 100644 --- a/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmToken.java +++ b/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmToken.java @@ -1,11 +1,14 @@ package com.zzang.chongdae.notification.domain; +import com.zzang.chongdae.member.repository.entity.MemberEntity; import lombok.Getter; -import lombok.RequiredArgsConstructor; @Getter -@RequiredArgsConstructor public class FcmToken { private final String value; + + public FcmToken(MemberEntity member) { + this.value = member.getFcmToken(); + } } diff --git a/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmTokens.java b/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmTokens.java index 627683444..2951feb15 100644 --- a/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmTokens.java +++ b/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmTokens.java @@ -13,7 +13,7 @@ private FcmTokens(List tokens) { public static FcmTokens from(List members) { List tokens = members.stream() - .map(member -> new FcmToken(member.getFcmToken())) + .map(FcmToken::new) .toList(); return new FcmTokens(tokens); } diff --git a/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmTopic.java b/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmTopic.java new file mode 100644 index 000000000..996e356a6 --- /dev/null +++ b/backend/src/main/java/com/zzang/chongdae/notification/domain/FcmTopic.java @@ -0,0 +1,31 @@ +package com.zzang.chongdae.notification.domain; + +import com.zzang.chongdae.offering.repository.entity.OfferingEntity; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class FcmTopic { + + private static final String TOPIC_FORMAT_MEMBER = "member"; + private static final String TOPIC_FORMAT_OFFERING_PROPOSER = "proposer_of_offering_%d"; + private static final String TOPIC_FORMAT_OFFERING_PARTICIPANT = "participant_of_offering_%d"; + + private final String value; + + public static FcmTopic proposerTopic(OfferingEntity offering) { + String value = TOPIC_FORMAT_OFFERING_PROPOSER.formatted(offering.getId()); + return new FcmTopic(value); + } + + public static FcmTopic participantTopic(OfferingEntity offering) { + String value = TOPIC_FORMAT_OFFERING_PARTICIPANT.formatted(offering.getId()); + return new FcmTopic(value); + } + + public static FcmTopic memberTopic() { + return new FcmTopic(TOPIC_FORMAT_MEMBER); + } +} diff --git a/backend/src/main/java/com/zzang/chongdae/notification/domain/ParticipationNotification.java b/backend/src/main/java/com/zzang/chongdae/notification/domain/ParticipationNotification.java deleted file mode 100644 index 3cd9ca425..000000000 --- a/backend/src/main/java/com/zzang/chongdae/notification/domain/ParticipationNotification.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.zzang.chongdae.notification.domain; - -import com.google.firebase.messaging.Message; -import com.zzang.chongdae.member.repository.entity.MemberEntity; -import com.zzang.chongdae.notification.service.FcmMessageManager; -import com.zzang.chongdae.offering.repository.entity.OfferingEntity; -import com.zzang.chongdae.offeringmember.repository.entity.OfferingMemberEntity; - -public class ParticipationNotification { - - private final FcmMessageManager messageManager; - private final OfferingEntity offering; - private final MemberEntity proposer; - private final MemberEntity participant; - - public ParticipationNotification(FcmMessageManager messageManager, OfferingMemberEntity offeringMember) { - this.messageManager = messageManager; - this.offering = offeringMember.getOffering(); - this.proposer = offering.getMember(); - this.participant = offeringMember.getMember(); - } - - public Message messageWhenParticipate() { - return messageManager.createMessage( - new FcmToken(proposer.getFcmToken()), - offering.getTitle(), - participant.getNickname() + "이(가) 참여했습니다."); - } - - public Message messageWhenCancelParticipate() { - return messageManager.createMessage( - new FcmToken(proposer.getFcmToken()), - offering.getTitle(), - participant.getNickname() + "이(가) 참여를 취소하였습니다."); - } -} diff --git a/backend/src/main/java/com/zzang/chongdae/notification/domain/RoomStatusNotification.java b/backend/src/main/java/com/zzang/chongdae/notification/domain/RoomStatusNotification.java deleted file mode 100644 index 48ee600f6..000000000 --- a/backend/src/main/java/com/zzang/chongdae/notification/domain/RoomStatusNotification.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.zzang.chongdae.notification.domain; - -import com.google.firebase.messaging.Message; -import com.zzang.chongdae.notification.service.FcmMessageManager; -import com.zzang.chongdae.offering.repository.entity.OfferingEntity; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@RequiredArgsConstructor -public class RoomStatusNotification { - - public static final String TOPIC_FORMAT_OFFERING = "/topics/%d"; // todo: topic 도메인 추출 - public static final String TOPIC_FORMAT_OFFERING_PROPOSER = "/topics/%d/proposer"; - - private static final String CONDITION_FORMAT = "'%s' in topics && !('%s' in topics)"; // todo: 부정문 가능한지 안드 테스트 - - private final FcmMessageManager messageManager; - private final OfferingEntity offering; - - public Message messageWhenUpdateStatus() { - String offeringTopic = TOPIC_FORMAT_OFFERING.formatted(offering.getId()); - String isProposerTopic = TOPIC_FORMAT_OFFERING_PROPOSER.formatted(offering.getId()); - String condition = CONDITION_FORMAT.formatted(offeringTopic, isProposerTopic); - log.info("[{}] 조건에 보내는 메시지", condition); - return messageManager.createMessage( - new FcmCondition(condition), - offering.getTitle(), - "거래 상태가 %s로 변경되었습니다.".formatted(offering.getRoomStatus())); - } -} diff --git a/backend/src/main/java/com/zzang/chongdae/notification/exception/NotificationErrorCode.java b/backend/src/main/java/com/zzang/chongdae/notification/exception/NotificationErrorCode.java index ed332cc4d..c76881b97 100644 --- a/backend/src/main/java/com/zzang/chongdae/notification/exception/NotificationErrorCode.java +++ b/backend/src/main/java/com/zzang/chongdae/notification/exception/NotificationErrorCode.java @@ -12,6 +12,7 @@ public enum NotificationErrorCode implements ErrorResponse { CANNOT_SEND_ALARM(HttpStatus.INTERNAL_SERVER_ERROR, "FCM 알림 전송에 실패하였습니다."), CANNOT_FIND_URL(HttpStatus.INTERNAL_SERVER_ERROR, "해당 URL을 찾을 수 없습니다."), + INVALID_COMMENT_ROOM_STATUS(HttpStatus.INTERNAL_SERVER_ERROR, "존재하지 않는 거래 상태입니다."), ; private final HttpStatus status; @@ -19,6 +20,6 @@ public enum NotificationErrorCode implements ErrorResponse { @Override public ErrorMessage getErrorMessage() { - return null; + return new ErrorMessage(this.message); } } diff --git a/backend/src/main/java/com/zzang/chongdae/notification/service/FcmMessageManager.java b/backend/src/main/java/com/zzang/chongdae/notification/service/FcmMessageManager.java deleted file mode 100644 index fa0a7c797..000000000 --- a/backend/src/main/java/com/zzang/chongdae/notification/service/FcmMessageManager.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.zzang.chongdae.notification.service; - -import com.google.firebase.messaging.Message; -import com.google.firebase.messaging.MulticastMessage; -import com.google.firebase.messaging.Notification; -import com.zzang.chongdae.notification.domain.FcmCondition; -import com.zzang.chongdae.notification.domain.FcmToken; -import com.zzang.chongdae.notification.domain.FcmTokens; -import org.springframework.stereotype.Service; - -@Service -public class FcmMessageManager { - - public Message createMessage(FcmToken token, String title, String body) { - return Message.builder() - .setToken(token.getValue()) - .setNotification(notification(title, body)) - .build(); - } - - public Message createMessage(FcmCondition condition, String title, String body) { - return Message.builder() - .setCondition(condition.getValue()) - .setNotification( - notification(title, body)) - .build(); - } - - public MulticastMessage createMessages(FcmTokens tokens, String title, String body) { - return MulticastMessage.builder() - .addAllTokens(tokens.getTokenValues()) - .setNotification(notification(title, body)) - .build(); - } - - private Notification notification(String title, String body) { - return Notification.builder() - .setTitle(title) - .setBody(body) - .build(); - } -} diff --git a/backend/src/main/java/com/zzang/chongdae/notification/service/FcmNotificationSender.java b/backend/src/main/java/com/zzang/chongdae/notification/service/FcmNotificationSender.java index a54d05bac..6acf1105f 100644 --- a/backend/src/main/java/com/zzang/chongdae/notification/service/FcmNotificationSender.java +++ b/backend/src/main/java/com/zzang/chongdae/notification/service/FcmNotificationSender.java @@ -14,6 +14,9 @@ @Component public class FcmNotificationSender implements NotificationSender { + private static final String ERROR_MESSAGE_WHEN_INVALID_TOKEN = "The registration token is not a valid FCM registration token"; + private static final String ERROR_MESSAGE_WHEN_OLD_TOKEN = "Requested entity was not found."; + @Override public String send(Message message) { try { @@ -21,12 +24,25 @@ public String send(Message message) { log.info("알림 메시지 전송 성공: {}", response); return response; } catch (FirebaseMessagingException e) { - log.error("알림 메시지 전송 실패: {}", e.getMessage()); - e.printStackTrace(); - throw new MarketException(NotificationErrorCode.CANNOT_SEND_ALARM); + return sendWhenFail(e); } } + private String sendWhenFail(FirebaseMessagingException e) { + if (isInvalidToken(e)) { + log.error("알림 메시지 전송 실패: {}", "유효하지 않은 토큰"); + return ""; + } + log.error("알림 메시지 전송 실패: {}", e.getMessage()); + e.printStackTrace(); + throw new MarketException(NotificationErrorCode.CANNOT_SEND_ALARM); + } + + private boolean isInvalidToken(FirebaseMessagingException e) { + return e.getMessage().contains(ERROR_MESSAGE_WHEN_INVALID_TOKEN) + || e.getMessage().contains(ERROR_MESSAGE_WHEN_OLD_TOKEN); + } + @Override public BatchResponse send(MulticastMessage message) { try { diff --git a/backend/src/main/java/com/zzang/chongdae/notification/service/FcmNotificationService.java b/backend/src/main/java/com/zzang/chongdae/notification/service/FcmNotificationService.java index 891f578c6..ccb327c66 100644 --- a/backend/src/main/java/com/zzang/chongdae/notification/service/FcmNotificationService.java +++ b/backend/src/main/java/com/zzang/chongdae/notification/service/FcmNotificationService.java @@ -1,15 +1,15 @@ package com.zzang.chongdae.notification.service; -import static com.zzang.chongdae.notification.domain.RoomStatusNotification.TOPIC_FORMAT_OFFERING; -import static com.zzang.chongdae.notification.domain.RoomStatusNotification.TOPIC_FORMAT_OFFERING_PROPOSER; - import com.google.firebase.messaging.BatchResponse; import com.google.firebase.messaging.Message; import com.google.firebase.messaging.MulticastMessage; import com.zzang.chongdae.comment.repository.entity.CommentEntity; -import com.zzang.chongdae.notification.domain.CommentNotification; -import com.zzang.chongdae.notification.domain.ParticipationNotification; -import com.zzang.chongdae.notification.domain.RoomStatusNotification; +import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.notification.domain.FcmTopic; +import com.zzang.chongdae.notification.service.message.CommentMessageManager; +import com.zzang.chongdae.notification.service.message.OfferingMessageManager; +import com.zzang.chongdae.notification.service.message.ParticipationMessageManager; +import com.zzang.chongdae.notification.service.message.RoomStatusMessageManager; import com.zzang.chongdae.offering.repository.entity.OfferingEntity; import com.zzang.chongdae.offeringmember.repository.entity.OfferingMemberEntity; import java.util.List; @@ -23,52 +23,56 @@ @Service public class FcmNotificationService { - private final FcmMessageManager messageManager; private final NotificationSender notificationSender; private final NotificationSubscriber notificationSubscriber; + private final CommentMessageManager commentMessageManager; + private final OfferingMessageManager offeringMessageManager; + private final ParticipationMessageManager participationMessageManager; + private final RoomStatusMessageManager roomStatusMessageManager; // TODO: 의존성 리팩터링 - public String participate(OfferingMemberEntity offeringMember) { // todo: naming - ParticipationNotification participationNotification = new ParticipationNotification(messageManager, - offeringMember); - Message message = participationNotification.messageWhenParticipate(); - notificationSubscriber.subscribe(offeringMember.getMember(), - TOPIC_FORMAT_OFFERING.formatted(offeringMember.getOffering().getId())); - return notificationSender.send(message); + public void participate(OfferingMemberEntity offeringMember) { + FcmTopic topic = FcmTopic.participantTopic(offeringMember.getOffering()); + notificationSubscriber.subscribe(offeringMember.getMember(), topic); + Message message = participationMessageManager.messageWhenParticipate(offeringMember); + notificationSender.send(message); } - public String cancelParticipation(OfferingMemberEntity offeringMember) { - ParticipationNotification participationNotification = new ParticipationNotification(messageManager, - offeringMember); - Message message = participationNotification.messageWhenCancelParticipate(); - notificationSubscriber.unsubscribe(offeringMember.getMember(), - TOPIC_FORMAT_OFFERING.formatted(offeringMember.getOffering().getId())); - return notificationSender.send(message); + public void cancelParticipation(OfferingMemberEntity offeringMember) { + FcmTopic topic = FcmTopic.participantTopic(offeringMember.getOffering()); + notificationSubscriber.unsubscribe(offeringMember.getMember(), topic); + Message message = participationMessageManager.messageWhenCancelParticipate(offeringMember); + notificationSender.send(message); } - public String updateStatus(OfferingEntity offering) { - RoomStatusNotification notification = new RoomStatusNotification(messageManager, offering); - Message message = notification.messageWhenUpdateStatus(); - return notificationSender.send(message); + public void updateStatus(OfferingEntity offering) { + Message message = roomStatusMessageManager.messageWhenUpdateStatus(offering); + notificationSender.send(message); } public void saveOffering(OfferingEntity offering) { - notificationSubscriber.subscribe(offering.getMember(), - TOPIC_FORMAT_OFFERING_PROPOSER.formatted(offering.getId())); + Message message = offeringMessageManager.messageWhenSaveOffering(offering); + FcmTopic topic = FcmTopic.proposerTopic(offering); + notificationSubscriber.subscribe(offering.getMember(), topic); + notificationSender.send(message); } public void deleteOffering(OfferingEntity offering) { - notificationSubscriber.unsubscribe(offering.getMember(), - TOPIC_FORMAT_OFFERING_PROPOSER.formatted(offering.getId())); + FcmTopic topic = FcmTopic.proposerTopic(offering); + notificationSubscriber.unsubscribe(offering.getMember(), topic); } @Nullable public BatchResponse saveComment(CommentEntity comment, List offeringMembers) { // todo: 참여자 도메인 추출 - CommentNotification notification = new CommentNotification(messageManager, comment, offeringMembers); - MulticastMessage message = notification.messageWhenSaveComment(); + MulticastMessage message = commentMessageManager.messageWhenSaveComment(comment, offeringMembers); if (message == null) { return null; } return notificationSender.send(message); } + + public void login(MemberEntity member) { + FcmTopic topic = FcmTopic.memberTopic(); + notificationSubscriber.subscribe(member, topic); + } } diff --git a/backend/src/main/java/com/zzang/chongdae/notification/service/FcmNotificationSubscriber.java b/backend/src/main/java/com/zzang/chongdae/notification/service/FcmNotificationSubscriber.java index c3cfa11c2..1283a78f5 100644 --- a/backend/src/main/java/com/zzang/chongdae/notification/service/FcmNotificationSubscriber.java +++ b/backend/src/main/java/com/zzang/chongdae/notification/service/FcmNotificationSubscriber.java @@ -4,6 +4,7 @@ import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.TopicManagementResponse; import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.notification.domain.FcmTopic; import java.util.List; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -13,27 +14,31 @@ public class FcmNotificationSubscriber implements NotificationSubscriber { @Override - public TopicManagementResponse subscribe(MemberEntity member, String topic) { + public TopicManagementResponse subscribe(MemberEntity member, FcmTopic topic) { try { TopicManagementResponse response = FirebaseMessaging.getInstance() - .subscribeToTopic(List.of(member.getFcmToken()), topic); + .subscribeToTopic(List.of(member.getFcmToken()), topic.getValue()); log.info("구독 성공 개수: {}", response.getSuccessCount()); log.info("구독 실패 개수: {}", response.getFailureCount()); return response; } catch (FirebaseMessagingException e) { + log.error("토픽 구독 실패: {}", e.getMessage()); + e.printStackTrace(); throw new RuntimeException(e); } } @Override - public TopicManagementResponse unsubscribe(MemberEntity member, String topic) { + public TopicManagementResponse unsubscribe(MemberEntity member, FcmTopic topic) { try { TopicManagementResponse response = FirebaseMessaging.getInstance() - .unsubscribeFromTopic(List.of(member.getFcmToken()), topic); + .unsubscribeFromTopic(List.of(member.getFcmToken()), topic.getValue()); log.info("구독 취소 성공 개수: {}", response.getSuccessCount()); log.info("구독 취소 실패 개수: {}", response.getFailureCount()); return response; } catch (FirebaseMessagingException e) { + log.error("토픽 구독 실패: {}", e.getMessage()); + e.printStackTrace(); throw new RuntimeException(e); } } diff --git a/backend/src/main/java/com/zzang/chongdae/notification/service/NotificationSubscriber.java b/backend/src/main/java/com/zzang/chongdae/notification/service/NotificationSubscriber.java index b7e1bb623..81699d3fb 100644 --- a/backend/src/main/java/com/zzang/chongdae/notification/service/NotificationSubscriber.java +++ b/backend/src/main/java/com/zzang/chongdae/notification/service/NotificationSubscriber.java @@ -2,10 +2,11 @@ import com.google.firebase.messaging.TopicManagementResponse; import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.notification.domain.FcmTopic; public interface NotificationSubscriber { - TopicManagementResponse subscribe(MemberEntity member, String topic); + TopicManagementResponse subscribe(MemberEntity member, FcmTopic topic); - TopicManagementResponse unsubscribe(MemberEntity member, String topic); + TopicManagementResponse unsubscribe(MemberEntity member, FcmTopic topic); } diff --git a/backend/src/main/java/com/zzang/chongdae/notification/service/message/CommentMessageManager.java b/backend/src/main/java/com/zzang/chongdae/notification/service/message/CommentMessageManager.java new file mode 100644 index 000000000..94e39e6f7 --- /dev/null +++ b/backend/src/main/java/com/zzang/chongdae/notification/service/message/CommentMessageManager.java @@ -0,0 +1,43 @@ +package com.zzang.chongdae.notification.service.message; + +import com.google.firebase.messaging.MulticastMessage; +import com.zzang.chongdae.comment.repository.entity.CommentEntity; +import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.notification.domain.FcmData; +import com.zzang.chongdae.notification.domain.FcmTokens; +import com.zzang.chongdae.offeringmember.repository.entity.OfferingMemberEntity; +import java.util.List; +import javax.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class CommentMessageManager { + + private static final String MESSAGE_BODY_FORMAT = "%s: %s"; + private static final String MESSAGE_TYPE = "comment_detail"; + + private final FcmMessageCreator messageCreator; + + @Nullable + public MulticastMessage messageWhenSaveComment(CommentEntity comment, List offeringMembers) { + FcmTokens tokens = FcmTokens.from(membersNotWriter(comment.getMember(), offeringMembers)); + if (tokens.isEmpty()) { + return null; + } + FcmData data = new FcmData(); + data.addData("title", comment.getOffering().getTitle()); + data.addData("body", MESSAGE_BODY_FORMAT.formatted(comment.getMember().getNickname(), comment.getContent())); + data.addData("offering_id", comment.getOffering().getId()); + data.addData("type", MESSAGE_TYPE); + return messageCreator.createMessages(tokens, data); + } + + private List membersNotWriter(MemberEntity writer, List offeringMembers) { + return offeringMembers.stream() + .map(OfferingMemberEntity::getMember) + .filter(member -> !member.isSame(writer)) + .toList(); + } +} diff --git a/backend/src/main/java/com/zzang/chongdae/notification/service/message/FcmMessageCreator.java b/backend/src/main/java/com/zzang/chongdae/notification/service/message/FcmMessageCreator.java new file mode 100644 index 000000000..c9fdce9a2 --- /dev/null +++ b/backend/src/main/java/com/zzang/chongdae/notification/service/message/FcmMessageCreator.java @@ -0,0 +1,42 @@ +package com.zzang.chongdae.notification.service.message; + +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.MulticastMessage; +import com.zzang.chongdae.notification.domain.FcmCondition; +import com.zzang.chongdae.notification.domain.FcmData; +import com.zzang.chongdae.notification.domain.FcmToken; +import com.zzang.chongdae.notification.domain.FcmTokens; +import com.zzang.chongdae.notification.domain.FcmTopic; +import org.springframework.stereotype.Component; + +@Component +public class FcmMessageCreator { + + public Message createMessage(FcmToken token, FcmData data) { + return Message.builder() + .setToken(token.getValue()) + .putAllData(data.getData()) + .build(); + } + + public Message createMessage(FcmCondition condition, FcmData data) { + return Message.builder() + .setCondition(condition.getValue()) + .putAllData(data.getData()) + .build(); + } + + public Message createMessage(FcmTopic topic, FcmData data) { + return Message.builder() + .setTopic(topic.getValue()) + .putAllData(data.getData()) + .build(); + } + + public MulticastMessage createMessages(FcmTokens tokens, FcmData data) { + return MulticastMessage.builder() + .addAllTokens(tokens.getTokenValues()) + .putAllData(data.getData()) + .build(); + } +} diff --git a/backend/src/main/java/com/zzang/chongdae/notification/service/message/OfferingMessageManager.java b/backend/src/main/java/com/zzang/chongdae/notification/service/message/OfferingMessageManager.java new file mode 100644 index 000000000..fcabb615f --- /dev/null +++ b/backend/src/main/java/com/zzang/chongdae/notification/service/message/OfferingMessageManager.java @@ -0,0 +1,28 @@ +package com.zzang.chongdae.notification.service.message; + +import com.google.firebase.messaging.Message; +import com.zzang.chongdae.notification.domain.FcmCondition; +import com.zzang.chongdae.notification.domain.FcmData; +import com.zzang.chongdae.offering.repository.entity.OfferingEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class OfferingMessageManager { + + private static final String MESSAGE_TITLE = "두근두근 새로운 공모를 확인해보세요!"; + private static final String MESSAGE_TYPE = "offering_detail"; + + private final FcmMessageCreator messageCreator; + + public Message messageWhenSaveOffering(OfferingEntity offering) { + FcmCondition condition = FcmCondition.offeringCondition(offering); + FcmData data = new FcmData(); + data.addData("title", MESSAGE_TITLE); + data.addData("body", offering.getTitle()); + data.addData("offering_id", offering.getId()); + data.addData("type", MESSAGE_TYPE); + return messageCreator.createMessage(condition, data); + } +} diff --git a/backend/src/main/java/com/zzang/chongdae/notification/service/message/ParticipationMessageManager.java b/backend/src/main/java/com/zzang/chongdae/notification/service/message/ParticipationMessageManager.java new file mode 100644 index 000000000..c2413403f --- /dev/null +++ b/backend/src/main/java/com/zzang/chongdae/notification/service/message/ParticipationMessageManager.java @@ -0,0 +1,49 @@ +package com.zzang.chongdae.notification.service.message; + +import com.google.firebase.messaging.Message; +import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.notification.domain.FcmData; +import com.zzang.chongdae.notification.domain.FcmToken; +import com.zzang.chongdae.offering.repository.entity.OfferingEntity; +import com.zzang.chongdae.offeringmember.repository.entity.OfferingMemberEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class ParticipationMessageManager { + + private static final String MESSAGE_BODY_FORMAT_PARTICIPATE = "%s 님이 참여했습니다."; + private static final String MESSAGE_BODY_FORMAT_CANCEL = "%s 님이 참여를 취소했습니다."; + private static final String MESSAGE_TYPE = "comment_detail"; + + private final FcmMessageCreator messageCreator; + + public Message messageWhenParticipate(OfferingMemberEntity offeringMember) { + OfferingEntity offering = offeringMember.getOffering(); + MemberEntity proposer = offering.getMember(); + MemberEntity participant = offeringMember.getMember(); + + FcmToken token = new FcmToken(proposer); + FcmData data = new FcmData(); + data.addData("title", offering.getTitle()); + data.addData("body", MESSAGE_BODY_FORMAT_PARTICIPATE.formatted(participant.getNickname())); + data.addData("offering_id", offering.getId()); + data.addData("type", MESSAGE_TYPE); + return messageCreator.createMessage(token, data); + } + + public Message messageWhenCancelParticipate(OfferingMemberEntity offeringMember) { + OfferingEntity offering = offeringMember.getOffering(); + MemberEntity proposer = offering.getMember(); + MemberEntity participant = offeringMember.getMember(); + + FcmToken token = new FcmToken(proposer); + FcmData data = new FcmData(); + data.addData("title", offering.getTitle()); + data.addData("body", MESSAGE_BODY_FORMAT_CANCEL.formatted(participant.getNickname())); + data.addData("offering_id", offering.getId()); + data.addData("type", MESSAGE_TYPE); + return messageCreator.createMessage(token, data); + } +} diff --git a/backend/src/main/java/com/zzang/chongdae/notification/service/message/RoomStatusMessageManager.java b/backend/src/main/java/com/zzang/chongdae/notification/service/message/RoomStatusMessageManager.java new file mode 100644 index 000000000..3b370435d --- /dev/null +++ b/backend/src/main/java/com/zzang/chongdae/notification/service/message/RoomStatusMessageManager.java @@ -0,0 +1,57 @@ +package com.zzang.chongdae.notification.service.message; + +import com.google.firebase.messaging.Message; +import com.zzang.chongdae.global.exception.MarketException; +import com.zzang.chongdae.notification.domain.FcmData; +import com.zzang.chongdae.notification.domain.FcmTopic; +import com.zzang.chongdae.notification.exception.NotificationErrorCode; +import com.zzang.chongdae.offering.domain.CommentRoomStatus; +import com.zzang.chongdae.offering.repository.entity.OfferingEntity; +import java.util.Arrays; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class RoomStatusMessageManager { + + private static final String MESSAGE_TYPE = "comment_detail"; + + private final FcmMessageCreator messageCreator; + + public Message messageWhenUpdateStatus(OfferingEntity offering) { + FcmTopic topic = FcmTopic.participantTopic(offering); + FcmData data = new FcmData(); + data.addData("title", offering.getTitle()); + data.addData("body", CommentRoomStatusMapper.getView(offering.getRoomStatus())); + data.addData("offering_id", offering.getId()); + data.addData("type", MESSAGE_TYPE); + return messageCreator.createMessage(topic, data); + } + + private enum CommentRoomStatusMapper { + + DELETED(CommentRoomStatus.DELETED, ""), + GROUPING(CommentRoomStatus.GROUPING, ""), + BUYING(CommentRoomStatus.BUYING, "모집이 마감됐어요! 이제 총대가 물건을 구매할 거예요"), + TRADING(CommentRoomStatus.TRADING, "총대가 물건을 구매했어요! 이제 거래를 진행해보아요"), + DONE(CommentRoomStatus.DONE, "거래가 완료되었어요. 고마워요 :)"), + ; + + private final CommentRoomStatus status; + private final String view; + + CommentRoomStatusMapper(CommentRoomStatus status, String view) { + this.status = status; + this.view = view; + } + + public static String getView(CommentRoomStatus roomStatus) { + return Arrays.stream(values()) + .filter(v -> v.status.equals(roomStatus)) + .findAny() + .orElseThrow(() -> new MarketException(NotificationErrorCode.INVALID_COMMENT_ROOM_STATUS)) + .view; + } + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index c38275f89..afdf484dc 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -24,7 +24,7 @@ spring: console: enabled: true path: /h2-console - + springdoc: swagger-ui: path: swagger-ui.html @@ -76,3 +76,7 @@ management: path-mapping: health: health-check base-path: / + +fcm: + secret-key: + path: /fcm/chongdaemarket-fcm-key.json diff --git a/backend/src/test/java/com/zzang/chongdae/notification/domain/RoomStatusNotificationTest.java b/backend/src/test/java/com/zzang/chongdae/notification/domain/RoomStatusMessageManagerTest.java similarity index 67% rename from backend/src/test/java/com/zzang/chongdae/notification/domain/RoomStatusNotificationTest.java rename to backend/src/test/java/com/zzang/chongdae/notification/domain/RoomStatusMessageManagerTest.java index 2d86dacf0..856e092f8 100644 --- a/backend/src/test/java/com/zzang/chongdae/notification/domain/RoomStatusNotificationTest.java +++ b/backend/src/test/java/com/zzang/chongdae/notification/domain/RoomStatusMessageManagerTest.java @@ -5,16 +5,17 @@ import com.google.firebase.messaging.Message; import com.zzang.chongdae.global.service.ServiceTest; import com.zzang.chongdae.member.repository.entity.MemberEntity; -import com.zzang.chongdae.notification.service.FcmMessageManager; +import com.zzang.chongdae.notification.service.message.FcmMessageCreator; +import com.zzang.chongdae.notification.service.message.RoomStatusMessageManager; import com.zzang.chongdae.offering.repository.entity.OfferingEntity; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -class RoomStatusNotificationTest extends ServiceTest { +class RoomStatusMessageManagerTest extends ServiceTest { @Autowired - private FcmMessageManager messageManager; + private FcmMessageCreator messageCreator; @DisplayName("FCM에 전송할 메시지를 생성한다.") @Test @@ -22,10 +23,10 @@ void should_notNull_when_createMessage() { // given MemberEntity proposer = memberFixture.createMember("ever"); OfferingEntity offering = offeringFixture.createOffering(proposer); - RoomStatusNotification notification = new RoomStatusNotification(messageManager, offering); + RoomStatusMessageManager notification = new RoomStatusMessageManager(messageCreator); // when - Message message = notification.messageWhenUpdateStatus(); + Message message = notification.messageWhenUpdateStatus(offering); // then assertThat(message).isNotNull(); diff --git a/backend/src/test/java/com/zzang/chongdae/notification/service/FakeNotificationSubscriber.java b/backend/src/test/java/com/zzang/chongdae/notification/service/FakeNotificationSubscriber.java index ee0f1f3a1..0f47df313 100644 --- a/backend/src/test/java/com/zzang/chongdae/notification/service/FakeNotificationSubscriber.java +++ b/backend/src/test/java/com/zzang/chongdae/notification/service/FakeNotificationSubscriber.java @@ -2,6 +2,7 @@ import com.google.firebase.messaging.TopicManagementResponse; import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.notification.domain.FcmTopic; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,14 +11,14 @@ public class FakeNotificationSubscriber implements NotificationSubscriber { private static final Logger log = LoggerFactory.getLogger(FakeNotificationSubscriber.class); @Override - public TopicManagementResponse subscribe(MemberEntity member, String topic) { + public TopicManagementResponse subscribe(MemberEntity member, FcmTopic topic) { log.info("구독 성공 개수: {}", 1); log.info("구독 실패 개수: {}", 0); return null; } @Override - public TopicManagementResponse unsubscribe(MemberEntity member, String topic) { + public TopicManagementResponse unsubscribe(MemberEntity member, FcmTopic topic) { log.info("구독 취소 성공 개수: {}", 1); log.info("구독 취소 실패 개수: {}", 0); return null; diff --git a/backend/src/test/java/com/zzang/chongdae/notification/service/FcmNotificationSenderTest.java b/backend/src/test/java/com/zzang/chongdae/notification/service/FcmNotificationSenderTest.java new file mode 100644 index 000000000..7cfd684bb --- /dev/null +++ b/backend/src/test/java/com/zzang/chongdae/notification/service/FcmNotificationSenderTest.java @@ -0,0 +1,65 @@ +package com.zzang.chongdae.notification.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.firebase.messaging.BatchResponse; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.MulticastMessage; +import com.zzang.chongdae.global.service.ServiceTest; +import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.notification.domain.FcmData; +import com.zzang.chongdae.notification.domain.FcmToken; +import com.zzang.chongdae.notification.domain.FcmTokens; +import com.zzang.chongdae.notification.service.message.FcmMessageCreator; +import java.util.List; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class FcmNotificationSenderTest extends ServiceTest { + + private final String proposerToken = "youHaveToChangeThis1"; + private final String participant1Token = "youHaveToChangeThis2"; + private final String participant2Token = "youHaveToChangeThis3"; + + @Autowired + private FcmMessageCreator messageCreator; + + @Autowired + private FcmNotificationSender notificationSender; + + @Disabled + @DisplayName("FCM에 메시지를 전송할 수 있다.") + @Test + void should_sendNotificationToFcm() { + // given + MemberEntity proposer = memberFixture.createMember("proposer", proposerToken); + Message message = messageCreator.createMessage(new FcmToken(proposer), new FcmData()); + + // when + String messageId = notificationSender.send(message); + + // then + assertThat(messageId).contains("messages"); + } + + @Disabled + @DisplayName("FCM에 대량 메시지를 전송할 수 있다.") + @Test + void should_sendNotificationsToFcm() { + // given + MemberEntity participant1 = memberFixture.createMember("ever1", participant1Token); + MemberEntity participant2 = memberFixture.createMember("ever2", participant2Token); + + FcmTokens tokens = FcmTokens.from(List.of(participant1, participant2)); + MulticastMessage messages = messageCreator.createMessages(tokens, new FcmData()); + + // when + BatchResponse response = notificationSender.send(messages); + + // then + assertThat(response).isNotNull(); + assertThat(response.getSuccessCount()).isEqualTo(2); + } +} diff --git a/backend/src/test/java/com/zzang/chongdae/notification/service/FcmNotificationSubscriberTest.java b/backend/src/test/java/com/zzang/chongdae/notification/service/FcmNotificationSubscriberTest.java index 22ef1d154..6063521b8 100644 --- a/backend/src/test/java/com/zzang/chongdae/notification/service/FcmNotificationSubscriberTest.java +++ b/backend/src/test/java/com/zzang/chongdae/notification/service/FcmNotificationSubscriberTest.java @@ -5,6 +5,7 @@ import com.google.firebase.messaging.TopicManagementResponse; import com.zzang.chongdae.global.service.ServiceTest; import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.notification.domain.FcmTopic; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -15,11 +16,11 @@ class FcmNotificationSubscriberTest extends ServiceTest { @Test void should_getCount_when_successToSubscribe() { // given - MemberEntity member = memberFixture.createMember("ever", "invalidToken"); + MemberEntity member = memberFixture.createMember("ever", "invalidFcmToken"); FcmNotificationSubscriber subscriber = new FcmNotificationSubscriber(); // when - TopicManagementResponse response = subscriber.subscribe(member, "ever-topic"); + TopicManagementResponse response = subscriber.subscribe(member, FcmTopic.memberTopic()); // then assertThat(response.getSuccessCount()).isEqualTo(0); @@ -35,7 +36,7 @@ void should_getCount_when_failToSubscribe() { FcmNotificationSubscriber subscriber = new FcmNotificationSubscriber(); // when - TopicManagementResponse response = subscriber.subscribe(member, "ever-topic"); + TopicManagementResponse response = subscriber.subscribe(member, FcmTopic.memberTopic()); // then assertThat(response.getSuccessCount()).isEqualTo(1); diff --git a/backend/src/test/java/com/zzang/chongdae/notification/service/FcmNotificationTest.java b/backend/src/test/java/com/zzang/chongdae/notification/service/FcmNotificationTest.java deleted file mode 100644 index 99733a701..000000000 --- a/backend/src/test/java/com/zzang/chongdae/notification/service/FcmNotificationTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.zzang.chongdae.notification.service; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.google.firebase.messaging.BatchResponse; -import com.zzang.chongdae.comment.repository.entity.CommentEntity; -import com.zzang.chongdae.global.service.ServiceTest; -import com.zzang.chongdae.member.repository.entity.MemberEntity; -import com.zzang.chongdae.offering.repository.entity.OfferingEntity; -import com.zzang.chongdae.offeringmember.repository.entity.OfferingMemberEntity; -import java.util.List; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -class FcmNotificationTest extends ServiceTest { - - private final String proposerToken = "youHaveToChangeThis1"; - private final String participant1Token = "youHaveToChangeThis2"; - private final String participant2Token = "youHaveToChangeThis3"; - - @Autowired - private FcmNotificationService notificationService; - - @Disabled - @DisplayName("FCM에 메시지를 전송할 수 있다.") - @Test - void should_sendNotificationToFcm() { - // given - MemberEntity proposer = memberFixture.createMember("proposer", proposerToken); - MemberEntity participant = memberFixture.createMember("ever", participant1Token); - - OfferingEntity offering = offeringFixture.createOffering(proposer); - offeringMemberFixture.createProposer(proposer, offering); - OfferingMemberEntity offeringMember = offeringMemberFixture.createParticipant(participant, offering); - - // when - String messageId = notificationService.participate(offeringMember); - - // then - assertThat(messageId).contains("messages"); - } - - @Disabled - @DisplayName("FCM에 대량 메시지를 전송할 수 있다.") - @Test - void should_sendNotificationsToFcm() { - // given - MemberEntity proposer = memberFixture.createMember("proposer", proposerToken); - MemberEntity participant1 = memberFixture.createMember("ever1", participant1Token); - MemberEntity participant2 = memberFixture.createMember("ever2", participant2Token); - - OfferingEntity offering = offeringFixture.createOffering(proposer); - OfferingMemberEntity proposerOfferingMember = offeringMemberFixture.createProposer(proposer, offering); - OfferingMemberEntity participant1OfferingMember = offeringMemberFixture.createParticipant(participant1, - offering); - OfferingMemberEntity participant2OfferingMember = offeringMemberFixture.createParticipant(participant2, - offering); - CommentEntity comment = commentFixture.createComment(proposer, offering); - - // when - BatchResponse batchResponse = notificationService.saveComment(comment, - List.of(proposerOfferingMember, participant1OfferingMember, participant2OfferingMember)); - - // then - assertThat(batchResponse).isNotNull(); - assertThat(batchResponse.getSuccessCount()).isEqualTo(2); - } - - @Disabled - @DisplayName("참여자가 없는 방에 댓글을 작성할 경우 메시지를 전송하지 않는다.") - @Test - void should_notSendNotificationsToFcm_when_noParticipants() { - // given - MemberEntity proposer = memberFixture.createMember("proposer", proposerToken); - - OfferingEntity offering = offeringFixture.createOffering(proposer); - OfferingMemberEntity proposerOfferingMember = offeringMemberFixture.createProposer(proposer, offering); - CommentEntity comment = commentFixture.createComment(proposer, offering); - - // when - BatchResponse batchResponse = notificationService.saveComment(comment, List.of(proposerOfferingMember)); - - // then - assertThat(batchResponse).isNull(); - } -} diff --git a/backend/src/test/resources/application-test.yml b/backend/src/test/resources/application-test.yml index a39820fd9..47e4f02e4 100644 --- a/backend/src/test/resources/application-test.yml +++ b/backend/src/test/resources/application-test.yml @@ -18,3 +18,7 @@ security: refresh-secret-key: refreshSecretKey access-token-expired: 30m refresh-token-expired: 14d + +fcm: + secret-key: + path: /fcm/chongdaemarket-fcm-key.json