diff --git a/src/main/java/com/moing/backend/domain/board/application/service/CreateBoardUserCase.java b/src/main/java/com/moing/backend/domain/board/application/service/CreateBoardUserCase.java index 84139ae8..c6e401c0 100644 --- a/src/main/java/com/moing/backend/domain/board/application/service/CreateBoardUserCase.java +++ b/src/main/java/com/moing/backend/domain/board/application/service/CreateBoardUserCase.java @@ -5,18 +5,8 @@ import com.moing.backend.domain.board.application.mapper.BoardMapper; import com.moing.backend.domain.board.domain.entity.Board; import com.moing.backend.domain.board.domain.service.BoardSaveService; -import com.moing.backend.domain.boardRead.application.mapper.BoardReadMapper; import com.moing.backend.domain.boardRead.application.service.CreateBoardReadUserCase; -import com.moing.backend.domain.boardRead.domain.entity.BoardRead; -import com.moing.backend.domain.boardRead.domain.service.BoardReadSaveService; -import com.moing.backend.domain.member.domain.entity.Member; -import com.moing.backend.domain.member.domain.service.MemberGetService; import com.moing.backend.domain.team.application.service.CheckLeaderUserCase; -import com.moing.backend.domain.team.domain.entity.Team; -import com.moing.backend.domain.team.domain.service.TeamGetService; -import com.moing.backend.domain.teamMember.domain.entity.TeamMember; -import com.moing.backend.domain.teamMember.domain.service.TeamMemberGetService; -import com.moing.backend.global.config.security.dto.User; import com.moing.backend.global.response.BaseServiceResponse; import com.moing.backend.global.util.BaseService; import lombok.RequiredArgsConstructor; @@ -35,17 +25,22 @@ public class CreateBoardUserCase { private final BoardMapper boardMapper; private final CreateBoardReadUserCase createBoardReadUserCase; private final BaseService baseService; + private final SendBoardAlarmUserCase sendBoardAlarmUserCase; /** * 게시글 생성 */ public CreateBoardResponse createBoard(String socialId, Long teamId, CreateBoardRequest createBoardRequest) { + //1, 게시글 생성, 저장 BaseServiceResponse data=baseService.getCommonData(socialId, teamId); - boolean isLeader = checkLeaderUserCase.isTeamLeader(data.getMember(), data.getTeam()); + boolean isLeader = checkLeaderUserCase.isTeamLeader(data.getMember(), data.getTeam()); //작성자 리더 여부 Board board=boardSaveService.saveBoard(boardMapper.toBoard(data.getMember(), data.getTeamMember(), data.getTeam(), createBoardRequest, isLeader)); - //읽음 처리 + //2. 읽음 처리 - 생성한 사람은 무조건 읽음 createBoardReadUserCase.createBoardRead(data.getTeam(), data.getMember(), board); + + //3. 알림 보내기 - 공지인 경우 + sendBoardAlarmUserCase.sendNewUploadAlarm(data, board); return new CreateBoardResponse(board.getBoardId()); } } diff --git a/src/main/java/com/moing/backend/domain/board/application/service/DeleteBoardUserCase.java b/src/main/java/com/moing/backend/domain/board/application/service/DeleteBoardUserCase.java index 387e17da..0274a771 100644 --- a/src/main/java/com/moing/backend/domain/board/application/service/DeleteBoardUserCase.java +++ b/src/main/java/com/moing/backend/domain/board/application/service/DeleteBoardUserCase.java @@ -21,8 +21,11 @@ public class DeleteBoardUserCase { * 게시글 삭제 */ public void deleteBoard(String socialId, Long teamId, Long boardId){ + //1. 게시글 조회 BaseBoardServiceResponse data= baseBoardService.getCommonData(socialId,teamId,boardId); + //2. 작성자인 경우 if (data.getTeamMember() == data.getBoard().getTeamMember()) { + //3. 삭제 boardDeleteService.deleteBoard(data.getBoard()); } else throw new NotAuthByBoardException(); } diff --git a/src/main/java/com/moing/backend/domain/board/application/service/GetBoardUserCase.java b/src/main/java/com/moing/backend/domain/board/application/service/GetBoardUserCase.java index 56430dcc..e72be8e8 100644 --- a/src/main/java/com/moing/backend/domain/board/application/service/GetBoardUserCase.java +++ b/src/main/java/com/moing/backend/domain/board/application/service/GetBoardUserCase.java @@ -31,8 +31,9 @@ public class GetBoardUserCase { * 게시글 상세 조회 */ public GetBoardDetailResponse getBoardDetail(String socialId, Long teamId, Long boardId) { + // 1. 게시글 조회 BaseBoardServiceResponse data = baseBoardService.getCommonData(socialId, teamId, boardId); - //읽음 처리 + // 2. 읽음 처리 createBoardReadUserCase.createBoardRead(data.getTeam(), data.getMember(), data.getBoard()); return boardMapper.toBoardDetail(data.getBoard(), data.getTeamMember() == data.getBoard().getTeamMember()); } diff --git a/src/main/java/com/moing/backend/domain/board/application/service/SendBoardAlarmUserCase.java b/src/main/java/com/moing/backend/domain/board/application/service/SendBoardAlarmUserCase.java new file mode 100644 index 00000000..94efba16 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/application/service/SendBoardAlarmUserCase.java @@ -0,0 +1,42 @@ +package com.moing.backend.domain.board.application.service; + +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamMember.domain.service.TeamMemberGetService; +import com.moing.backend.global.config.fcm.dto.request.MultiRequest; +import com.moing.backend.global.config.fcm.service.FcmService; +import com.moing.backend.global.response.BaseServiceResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; +import java.util.Optional; + +import static com.moing.backend.global.config.fcm.constant.NewUploadTitle.UPLOAD_NOTICE_NEW_TITLE; + +@Service +@RequiredArgsConstructor +@Transactional +public class SendBoardAlarmUserCase { + + private final TeamMemberGetService teamMemberGetService; + private final FcmService fcmService; + + public void sendNewUploadAlarm(BaseServiceResponse baseServiceResponse, Board board) { + Member member = baseServiceResponse.getMember(); + Team team = baseServiceResponse.getTeam(); + + if (board.isNotice() && member.isNewUploadPush()) { + String title = team.getName() + " " + UPLOAD_NOTICE_NEW_TITLE.getTitle(); + String message = board.getTitle(); + Optional> fcmTokens = teamMemberGetService.getFcmTokensExceptMe(team.getTeamId(), member.getMemberId()); + if (fcmTokens.isPresent() && !fcmTokens.get().isEmpty()) { + MultiRequest toMultiRequest = new MultiRequest(fcmTokens.get(), title, message); + fcmService.sendMultipleDevices(toMultiRequest); + } + } + } +} + diff --git a/src/main/java/com/moing/backend/domain/board/application/service/UpdateBoardUserCase.java b/src/main/java/com/moing/backend/domain/board/application/service/UpdateBoardUserCase.java index 394b34e3..21e20b91 100644 --- a/src/main/java/com/moing/backend/domain/board/application/service/UpdateBoardUserCase.java +++ b/src/main/java/com/moing/backend/domain/board/application/service/UpdateBoardUserCase.java @@ -21,8 +21,11 @@ public class UpdateBoardUserCase { * 게시글 수정 */ public UpdateBoardResponse updateBoard(String socialId, Long teamId, Long boardId, UpdateBoardRequest updateBoardRequest){ + // 1. 게시글 조회 BaseBoardServiceResponse data= baseBoardService.getCommonData(socialId, teamId, boardId); + // 2. 게시글 작성자만 if (data.getTeamMember() == data.getBoard().getTeamMember()) { + // 3. 수정 data.getBoard().updateBoard(updateBoardRequest); return new UpdateBoardResponse(data.getBoard().getBoardId()); } else throw new NotAuthByBoardException(); diff --git a/src/main/java/com/moing/backend/domain/boardComment/application/service/CreateBoardCommentUserCase.java b/src/main/java/com/moing/backend/domain/boardComment/application/service/CreateBoardCommentUserCase.java index 759fc5c4..23e1d12b 100644 --- a/src/main/java/com/moing/backend/domain/boardComment/application/service/CreateBoardCommentUserCase.java +++ b/src/main/java/com/moing/backend/domain/boardComment/application/service/CreateBoardCommentUserCase.java @@ -21,9 +21,14 @@ public class CreateBoardCommentUserCase { private final BoardCommentMapper boardCommentMapper; private final BaseBoardService baseBoardService; + /** + * 게시글 댓글 생성 + */ public CreateBoardCommentResponse createBoardComment(String socialId, Long teamId, Long boardId, CreateBoardCommentRequest createBoardCommentRequest) { + // 1. 게시글 댓글 생성 BaseBoardServiceResponse data = baseBoardService.getCommonData(socialId, teamId, boardId); BoardComment boardComment = boardCommentSaveService.saveBoardComment(boardCommentMapper.toBoardComment(data.getTeamMember(), data.getBoard(), createBoardCommentRequest)); + // 2. 게시글 댓글 개수 증가 data.getBoard().incrComNum(); return new CreateBoardCommentResponse(boardComment.getBoardCommentId()); } diff --git a/src/main/java/com/moing/backend/domain/boardComment/application/service/DeleteBoardCommentUserCase.java b/src/main/java/com/moing/backend/domain/boardComment/application/service/DeleteBoardCommentUserCase.java index 6201717f..2f0285c3 100644 --- a/src/main/java/com/moing/backend/domain/boardComment/application/service/DeleteBoardCommentUserCase.java +++ b/src/main/java/com/moing/backend/domain/boardComment/application/service/DeleteBoardCommentUserCase.java @@ -20,11 +20,19 @@ public class DeleteBoardCommentUserCase { private final BoardCommentDeleteService boardCommentDeleteService; private final BaseBoardService baseBoardService; + /** + * 게시글 댓글 삭제 + */ + public void deleteBoardComment(String socialId, Long teamId, Long boardId, Long boardCommentId){ + // 1. 게시글 댓글 조회 BaseBoardServiceResponse data = baseBoardService.getCommonData(socialId, teamId, boardId); BoardComment boardComment=boardCommentGetService.getBoardComment(boardCommentId); + // 2. 게시글 댓글 작성자만 if (data.getTeamMember() == boardComment.getTeamMember()) { + // 3. 삭제 boardCommentDeleteService.deleteBoardComment(boardComment); + // 4. 댓글 개수 줄이기 data.getBoard().decrComNum(); } else throw new NotAuthByBoardCommentException(); } diff --git a/src/main/java/com/moing/backend/domain/boardComment/application/service/GetBoardCommentUserCase.java b/src/main/java/com/moing/backend/domain/boardComment/application/service/GetBoardCommentUserCase.java index df53c6e1..544c5174 100644 --- a/src/main/java/com/moing/backend/domain/boardComment/application/service/GetBoardCommentUserCase.java +++ b/src/main/java/com/moing/backend/domain/boardComment/application/service/GetBoardCommentUserCase.java @@ -17,6 +17,9 @@ public class GetBoardCommentUserCase { private final BoardCommentGetService boardCommentGetService; private final BaseBoardService baseBoardService; + /** + * 게시글 댓글 전체 조회 + */ public GetBoardCommentResponse getBoardCommentAll(String socialId, Long teamId, Long boardId){ BaseBoardServiceResponse data = baseBoardService.getCommonData(socialId, teamId, boardId); return boardCommentGetService.getBoardCommentAll(boardId, data.getTeamMember()); diff --git a/src/main/java/com/moing/backend/domain/team/application/service/SendTeamAlarmUserCase.java b/src/main/java/com/moing/backend/domain/team/application/service/SendTeamAlarmUserCase.java new file mode 100644 index 00000000..e5ef6690 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/service/SendTeamAlarmUserCase.java @@ -0,0 +1,43 @@ +package com.moing.backend.domain.team.application.service; + +import com.moing.backend.domain.teamMember.domain.service.TeamMemberGetService; +import com.moing.backend.global.config.fcm.dto.request.MultiRequest; +import com.moing.backend.global.config.fcm.service.FcmService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; +import java.util.Optional; + +import static com.moing.backend.global.config.fcm.constant.NewUploadTitle.UPLOAD_NOTICE_NEW_TITLE; + +@Service +@Transactional +@RequiredArgsConstructor +public class SendTeamAlarmUserCase { + private final TeamMemberGetService teamMemberGetService; + private final FcmService fcmService; + + public void sendApprovalAlarm(Long teamId) { + //TODO: 승인, 반려 문구 constant로 + String title = "소모임 승인 안내"; + String message = "소모임이 승인되었습니다."; + sendAlarm(teamId, title, message); + } + + public void sendRejectionAlarm(Long teamId) { + String title = "소모임 반려 안내"; + String message = "소모임이 반려되었습니다."; + sendAlarm(teamId, title, message); + } + + private void sendAlarm(Long teamId, String title, String message) { + Optional> fcmTokens = teamMemberGetService.getFcmTokens(teamId); + if(fcmTokens.isPresent() && !fcmTokens.get().isEmpty()) { + MultiRequest toMultiRequest = new MultiRequest(fcmTokens.get(), title, message); + fcmService.sendMultipleDevices(toMultiRequest); + } + } +} + diff --git a/src/main/java/com/moing/backend/domain/team/presentation/AdminTeamController.java b/src/main/java/com/moing/backend/domain/team/presentation/AdminTeamController.java index 3241479e..5ffa70f9 100644 --- a/src/main/java/com/moing/backend/domain/team/presentation/AdminTeamController.java +++ b/src/main/java/com/moing/backend/domain/team/presentation/AdminTeamController.java @@ -1,18 +1,45 @@ package com.moing.backend.domain.team.presentation; -import com.moing.backend.domain.team.application.service.CreateTeamUserCase; +import com.moing.backend.domain.team.application.service.SendTeamAlarmUserCase; +import com.moing.backend.global.response.SuccessResponse; import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import static com.moing.backend.domain.team.presentation.constant.TeamResponseMessage.*; + @RestController @AllArgsConstructor @RequestMapping("/api/admin/team") public class AdminTeamController { - //TODO 소모임 승인, 반려하기 - private final CreateTeamUserCase createTeamService; + private final SendTeamAlarmUserCase sendTeamAlarmUserCase; + + /** + * 소모임 승인 알림 보내기 + * [POST] api/admin/team/approval/{teamId} + * 작성자 : 김민수 + */ + + @PostMapping("/approval/{teamId}") + public ResponseEntity sendApproveAlarm(@PathVariable Long teamId) { + this.sendTeamAlarmUserCase.sendApprovalAlarm(teamId); + return ResponseEntity.ok(SuccessResponse.create(SEND_APPROVAL_ALARM_SUCCESS.getMessage())); + } + /** + * 소모임 반려 알림 보내기 + * [POST] api/admin/team/rejection/{teamId} + * 작성자: 김민수 + */ + @PostMapping("/rejection/{teamId}") + public ResponseEntity sendRejectionAlarm(@PathVariable Long teamId){ + this.sendTeamAlarmUserCase.sendRejectionAlarm(teamId); + return ResponseEntity.ok(SuccessResponse.create(SEND_REJECTION_ALARM_SUCCESS.getMessage())); + } } diff --git a/src/main/java/com/moing/backend/domain/team/presentation/constant/TeamResponseMessage.java b/src/main/java/com/moing/backend/domain/team/presentation/constant/TeamResponseMessage.java index 45ae0eb5..2b1ccdcb 100644 --- a/src/main/java/com/moing/backend/domain/team/presentation/constant/TeamResponseMessage.java +++ b/src/main/java/com/moing/backend/domain/team/presentation/constant/TeamResponseMessage.java @@ -10,6 +10,8 @@ public enum TeamResponseMessage { GET_TEAM_SUCCESS("홈 화면에서 내 소모임을 모두 조회했습니다."), SIGNIN_TEAM_SUCCESS("소모임에 가입하였습니다"), DISBAND_TEAM_SUCCESS("[소모임장 권한] 소모임을 강제 종료했습니다."), - WITHDRAW_TEAM_SUCCESS("[소모임원 권한] 소모임을 탈퇴하였습니다"); + WITHDRAW_TEAM_SUCCESS("[소모임원 권한] 소모임을 탈퇴하였습니다"), + SEND_APPROVAL_ALARM_SUCCESS("소모임 승인 알림을 보냈습니다."), + SEND_REJECTION_ALARM_SUCCESS("소모임 반려 알림을 보냈습니다."); private final String message; } diff --git a/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberCustomRepository.java b/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberCustomRepository.java index 4c7f0370..725b70e1 100644 --- a/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberCustomRepository.java +++ b/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberCustomRepository.java @@ -1,7 +1,10 @@ package com.moing.backend.domain.teamMember.domain.repository; import java.util.List; +import java.util.Optional; public interface TeamMemberCustomRepository { List findMemberIdsByTeamId(Long teamId); + Optional> findFcmTokensByTeamIdAndMemberId(Long teamId, Long memberId); + Optional> findFcmTokensByTeamId(Long teamId); } diff --git a/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberCustomRepositoryImpl.java b/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberCustomRepositoryImpl.java index 880d3e73..40f64eaa 100644 --- a/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberCustomRepositoryImpl.java +++ b/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberCustomRepositoryImpl.java @@ -5,6 +5,7 @@ import javax.persistence.EntityManager; import java.util.List; +import java.util.Optional; import static com.moing.backend.domain.teamMember.domain.entity.QTeamMember.teamMember; @@ -24,4 +25,23 @@ public List findMemberIdsByTeamId(Long teamId) { .where(teamMember.team.isDeleted.eq(false)) .fetch(); } + + @Override + public Optional> findFcmTokensByTeamIdAndMemberId(Long teamId, Long memberId) { + return Optional.ofNullable(queryFactory.select(teamMember.member.fcmToken) + .from(teamMember) + .where(teamMember.team.teamId.eq(teamId)) //해당 소모임에 참여하고 있고 + .where(teamMember.member.isNewUploadPush.eq(true)) //알림 설정 on해 있고 + .where(teamMember.member.memberId.ne(memberId)) //지금 유저가 아닌 경우 + .fetch()); + } + + @Override + public Optional> findFcmTokensByTeamId(Long teamId) { + return Optional.ofNullable(queryFactory.select(teamMember.member.fcmToken) + .from(teamMember) + .where(teamMember.team.teamId.eq(teamId)) //해당 소모임에 참여하고 있고 + .where(teamMember.member.isNewUploadPush.eq(true)) //알림 설정 on해 있고 + .fetch()); + } } diff --git a/src/main/java/com/moing/backend/domain/teamMember/domain/service/TeamMemberGetService.java b/src/main/java/com/moing/backend/domain/teamMember/domain/service/TeamMemberGetService.java index e224a89a..6ba8f3db 100644 --- a/src/main/java/com/moing/backend/domain/teamMember/domain/service/TeamMemberGetService.java +++ b/src/main/java/com/moing/backend/domain/teamMember/domain/service/TeamMemberGetService.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import java.util.List; +import java.util.Optional; @DomainService @RequiredArgsConstructor @@ -22,4 +23,13 @@ public List getTeamMemberIds(Long teamId){ public TeamMember getTeamMember(Member member, Team team){ return teamMemberRepository.findTeamMemberByTeamAndMember(team, member).orElseThrow(()-> new NotFoundByTeamIdException()); } + + public Optional> getFcmTokensExceptMe(Long teamId, Long memberId) { + return teamMemberRepository.findFcmTokensByTeamIdAndMemberId(teamId, memberId); + } + + public Optional> getFcmTokens(Long teamId) { + return teamMemberRepository.findFcmTokensByTeamId(teamId); + } + } diff --git a/src/main/java/com/moing/backend/global/config/fcm/FcmConfig.java b/src/main/java/com/moing/backend/global/config/fcm/FcmConfig.java new file mode 100644 index 00000000..711c4290 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/FcmConfig.java @@ -0,0 +1,59 @@ +package com.moing.backend.global.config.fcm; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import com.moing.backend.global.config.fcm.exception.InitializeException; +import com.moing.backend.global.config.fcm.exception.MessagingException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +@Configuration +@Slf4j +public class FcmConfig { + + + @Value("${firebase.config.path}") + private String firebaseConfigPath; + + @Value("${firebase.config.projectId}") + private String projectId; + + @Bean + public FirebaseApp firebaseApp() { + try { + FileInputStream serviceAccount = new FileInputStream(firebaseConfigPath); + + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(serviceAccount)) + .setProjectId(projectId) + .build(); + + return FirebaseApp.initializeApp(options); + } catch (FileNotFoundException e) { + throw new IllegalStateException("파일을 찾을 수 없습니다." + e.getMessage()); + } catch (IOException e) { + throw new InitializeException(); + } + } + + @Bean + public FirebaseMessaging firebaseMessaging() { + try { + return FirebaseMessaging.getInstance(firebaseApp()); + } catch (IllegalStateException e) { + throw new MessagingException("FirebaseApp 초기화에 실패하였습니다." + e.getMessage()); + } catch (NullPointerException e) { + throw new IllegalStateException("FirebaseApp을 불러오는데 실패하였습니다." + e.getMessage()); + } catch (Exception e) { + throw new IllegalArgumentException("firebaseConfigPath를 읽어오는데 실패하였습니다." + e.getMessage()); + } + } +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/constant/NewUploadTitle.java b/src/main/java/com/moing/backend/global/config/fcm/constant/NewUploadTitle.java new file mode 100644 index 00000000..98690142 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/constant/NewUploadTitle.java @@ -0,0 +1,13 @@ +package com.moing.backend.global.config.fcm.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NewUploadTitle{ + UPLOAD_NOTICE_NEW_TITLE("새로운 공지 알려드려요!"), + UPLOAD_VOTE_NEW_TITLE("새로운 투표 알려드려요!"), + UPLOAD_MISSION_NEW_TITLE("새로운 미션 알려드려요!"); + private final String title; +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/dto/request/MultiRequest.java b/src/main/java/com/moing/backend/global/config/fcm/dto/request/MultiRequest.java new file mode 100644 index 00000000..222e1258 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/dto/request/MultiRequest.java @@ -0,0 +1,23 @@ +package com.moing.backend.global.config.fcm.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class MultiRequest { + @NotNull(message = "기기 등록 토큰들을 입력해 주세요.") + private List registrationToken; + + @NotNull(message = "알림 제목을 입력해 주세요.") + private String title; + + @NotNull(message = "알림 내용을 입력해 주세요.") + private String body; + +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/dto/request/SingleRequest.java b/src/main/java/com/moing/backend/global/config/fcm/dto/request/SingleRequest.java new file mode 100644 index 00000000..01f2a37a --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/dto/request/SingleRequest.java @@ -0,0 +1,21 @@ +package com.moing.backend.global.config.fcm.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class SingleRequest { + @NotNull(message = "기기 등록 토큰을 입력해 주세요.") + private String registrationToken; + + @NotNull(message = "알림 제목을 입력해 주세요.") + private String title; + + @NotNull(message = "알림 내용을 입력해 주세요.") + private String body; +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/dto/response/MultiResponse.java b/src/main/java/com/moing/backend/global/config/fcm/dto/response/MultiResponse.java new file mode 100644 index 00000000..39f67767 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/dto/response/MultiResponse.java @@ -0,0 +1,14 @@ +package com.moing.backend.global.config.fcm.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class MultiResponse { + private final String response; + private final List failedTokens; + +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/config/fcm/dto/response/SingleResponse.java b/src/main/java/com/moing/backend/global/config/fcm/dto/response/SingleResponse.java new file mode 100644 index 00000000..b4f0cbf9 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/dto/response/SingleResponse.java @@ -0,0 +1,11 @@ +package com.moing.backend.global.config.fcm.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class SingleResponse { + private final String response; + +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/exception/FirebaseException.java b/src/main/java/com/moing/backend/global/config/fcm/exception/FirebaseException.java new file mode 100644 index 00000000..11c67de3 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/exception/FirebaseException.java @@ -0,0 +1,11 @@ +package com.moing.backend.global.config.fcm.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public abstract class FirebaseException extends ApplicationException { + protected FirebaseException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/exception/InitializeException.java b/src/main/java/com/moing/backend/global/config/fcm/exception/InitializeException.java new file mode 100644 index 00000000..34b560ca --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/exception/InitializeException.java @@ -0,0 +1,12 @@ +package com.moing.backend.global.config.fcm.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class InitializeException extends FirebaseException { + public InitializeException() { + super(ErrorCode.INITIALIZE_ERROR, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/exception/MessagingException.java b/src/main/java/com/moing/backend/global/config/fcm/exception/MessagingException.java new file mode 100644 index 00000000..0acc476b --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/exception/MessagingException.java @@ -0,0 +1,11 @@ +package com.moing.backend.global.config.fcm.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class MessagingException extends FirebaseException { + public MessagingException(String message) { + super(ErrorCode.MESSAGING_ERROR, + HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/exception/NotificationException.java b/src/main/java/com/moing/backend/global/config/fcm/exception/NotificationException.java new file mode 100644 index 00000000..b7dd8a90 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/exception/NotificationException.java @@ -0,0 +1,11 @@ +package com.moing.backend.global.config.fcm.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotificationException extends FirebaseException { + public NotificationException(String message) { + super(ErrorCode.NOTIFICATION_ERROR, + HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/service/FcmService.java b/src/main/java/com/moing/backend/global/config/fcm/service/FcmService.java new file mode 100644 index 00000000..74cdb54d --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/service/FcmService.java @@ -0,0 +1,151 @@ +package com.moing.backend.global.config.fcm.service; + +import com.google.firebase.messaging.*; +import com.moing.backend.global.config.fcm.dto.request.MultiRequest; +import com.moing.backend.global.config.fcm.dto.request.SingleRequest; +import com.moing.backend.global.config.fcm.dto.response.MultiResponse; +import com.moing.backend.global.config.fcm.dto.response.SingleResponse; +import com.moing.backend.global.config.fcm.exception.NotificationException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.ArrayList; +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +@Slf4j +public class FcmService { + + private final FirebaseMessaging firebaseMessaging; + + + public SingleResponse sendSingleDevice(SingleRequest toSingleRequest) { + + Notification notification = Notification.builder() + .setTitle(toSingleRequest.getTitle()) + .setBody(toSingleRequest.getBody()) + .build(); + + // Android Configuration + AndroidConfig androidConfig = AndroidConfig.builder() + .setPriority(AndroidConfig.Priority.HIGH) + .setNotification(AndroidNotification.builder() + .setChannelId("FCM_Channel") + .setTitle(toSingleRequest.getTitle()) + .setBody(toSingleRequest.getBody()) + .build()) + .build(); + + // APNs Configuration + ApnsConfig apnsConfig = ApnsConfig.builder() + .setAps(Aps.builder() + .setCategory("YOUR_CATEGORY") // Replace with your category + .setAlert(ApsAlert.builder() + .setTitle(toSingleRequest.getTitle()) + .setBody(toSingleRequest.getBody()) + .build()) + .build()) + .build(); + + Message message = Message.builder() + .setToken(toSingleRequest.getRegistrationToken()) + .setNotification(notification) + .setAndroidConfig(androidConfig) // Applying Android configuration + .setApnsConfig(apnsConfig) // Applying APNs configuration + .build(); + + try { + String response = firebaseMessaging.send(message); + return new SingleResponse(response); + } catch (FirebaseMessagingException e) { + throw handleException(e); + } + } + + + public MultiResponse sendMultipleDevices(MultiRequest toMultiRequest) { + + Notification notification = Notification.builder() + .setTitle(toMultiRequest.getTitle()) + .setBody(toMultiRequest.getBody()) + .build(); + + // Android Configuration + AndroidConfig androidConfig = AndroidConfig.builder() + .setPriority(AndroidConfig.Priority.HIGH) + .setNotification(AndroidNotification.builder() + .setChannelId("FCM_Channel") + .setTitle(toMultiRequest.getTitle()) + .setBody(toMultiRequest.getBody()) + .build()) + .build(); + + // APNs Configuration + ApnsConfig apnsConfig = ApnsConfig.builder() + .setAps(Aps.builder() + .setCategory("YOUR_CATEGORY") // Replace with your category + .setAlert(ApsAlert.builder() + .setTitle(toMultiRequest.getTitle()) + .setBody(toMultiRequest.getBody()) + .build()) + .build()) + .build(); + + MulticastMessage message = MulticastMessage.builder() + .addAllTokens(toMultiRequest.getRegistrationToken()) + .setNotification(notification) + .setAndroidConfig(androidConfig) // Applying Android configuration + .setApnsConfig(apnsConfig) // Applying APNs configuration + .build(); + + try { + BatchResponse response = firebaseMessaging.sendMulticast(message); + List failedTokens = new ArrayList<>(); + + if (response.getFailureCount() > 0) { + List responses = response.getResponses(); + + for (int i = 0; i < responses.size(); i++) { + if (!responses.get(i).isSuccessful()) { + // Add the failed tokens to the list + failedTokens.add(toMultiRequest.getRegistrationToken().get(i)); + } + } + } + + String messageString = String.format("%d messages were sent successfully.", response.getSuccessCount()); + + return new MultiResponse(messageString, failedTokens); + + } catch (FirebaseMessagingException e) { + throw handleException(e); + } + } + + + + private NotificationException handleException(FirebaseMessagingException e) { + String errorCode = e.getErrorCode().name(); + String errorMessage = e.getMessage(); + + switch (errorCode) { + case "INVALID_ARGUMENT": + return new NotificationException("올바르지 않은 인자 값입니다: " + errorMessage); + case "NOT_FOUND": + return new NotificationException("등록 토큰이 유효하지 않거나, 주제(Topic)가 존재하지 않습니다: " + errorMessage); + case "UNREGISTERED": + return new NotificationException("해당 주제(Topic)의 구독이 해지되었습니다: " + errorMessage); + case "UNAVAILABLE": + return new NotificationException("서비스를 사용할 수 없습니다: " + errorMessage); + default: + return new NotificationException("메시지 전송에 실패했습니다: " + errorMessage); + } + } + +} + + diff --git a/src/main/java/com/moing/backend/global/response/ErrorCode.java b/src/main/java/com/moing/backend/global/response/ErrorCode.java index 31aa29b6..e31bf8ed 100644 --- a/src/main/java/com/moing/backend/global/response/ErrorCode.java +++ b/src/main/java/com/moing/backend/global/response/ErrorCode.java @@ -13,6 +13,11 @@ public enum ErrorCode { INTERNAL_SERVER_ERROR("500", "서버에서 요청을 처리하는 동안 오류가 발생했습니다."), NOT_FOUND_REFRESH_TOKEN_ERROR( "J0008", "유효하지 않는 RefreshToken 입니다."), + //FCM 토큰 관련 + INITIALIZE_ERROR("F0001", "Firebase Admin SDK 초기화에 실패했습니다."), + NOTIFICATION_ERROR("F0002", "메시지 전송에 실패했습니다."), + MESSAGING_ERROR("F0003", "firebaseConfigPath를 읽어오는데 실패하였습니다"), + //유저 관련 에러 코드 NOT_FOUND_BY_SOCIAL_ID_ERROR( "U0001", "해당 socialId인 유저가 존재하지 않습니다."), ACCOUNT_ALREADY_EXIST("AU0001", "해당 email로 다른 소셜 플랫폼으로 가입하였습니다."),