diff --git a/.gitignore b/.gitignore index c714a9a7..f581f29a 100644 --- a/.gitignore +++ b/.gitignore @@ -41,5 +41,7 @@ out/ ### VS Code ### .vscode/ + /src/main/resources/**/*.yml /src/main/resources/**/*.yaml +/src/main/resources/firebase/**/*.json diff --git a/build.gradle b/build.gradle index 1c1efcf5..c3584ad6 100644 --- a/build.gradle +++ b/build.gradle @@ -47,9 +47,14 @@ dependencies { //swagger implementation 'org.springdoc:springdoc-openapi-ui:1.7.0' - + //sms implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.13' + //firebase + implementation 'com.google.firebase:firebase-admin:9.2.0' + + implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.9.0' + //querydsl 추가 implementation "com.querydsl:querydsl-jpa:5.0.0" annotationProcessor "com.querydsl:querydsl-apt:5.0.0" diff --git a/src/main/java/com/uspray/uspray/DTO/notification/FCMNotificationRequestDto.java b/src/main/java/com/uspray/uspray/DTO/notification/FCMNotificationRequestDto.java new file mode 100644 index 00000000..8ed74aa0 --- /dev/null +++ b/src/main/java/com/uspray/uspray/DTO/notification/FCMNotificationRequestDto.java @@ -0,0 +1,18 @@ +package com.uspray.uspray.DTO.notification; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class FCMNotificationRequestDto { + + @Schema(example = "device token") + private String token; + @Schema(example = "오펜하이머") + private String title; + @Schema(example = "나는 곧 쭈꾸미오") + private String body; + +} diff --git a/src/main/java/com/uspray/uspray/DTO/notification/NotificationAgreeDto.java b/src/main/java/com/uspray/uspray/DTO/notification/NotificationAgreeDto.java new file mode 100644 index 00000000..bf02b0dc --- /dev/null +++ b/src/main/java/com/uspray/uspray/DTO/notification/NotificationAgreeDto.java @@ -0,0 +1,15 @@ +package com.uspray.uspray.DTO.notification; + +import com.uspray.uspray.Enums.NotificationType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class NotificationAgreeDto { + + @Schema(example = "2") + private NotificationType notificationType; + @Schema(example = "false") + private Boolean agree; + +} diff --git a/src/main/java/com/uspray/uspray/Enums/NotificationType.java b/src/main/java/com/uspray/uspray/Enums/NotificationType.java new file mode 100644 index 00000000..710a5640 --- /dev/null +++ b/src/main/java/com/uspray/uspray/Enums/NotificationType.java @@ -0,0 +1,16 @@ +package com.uspray.uspray.Enums; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public enum NotificationType { + PRAY_TIME("기도합시다~", "기도 시간: 오전 8시"), + PRAY_FOR_ME("기도받았다~", "다른 사람이 내 기도 제목을 기도 했을 때"), + SHARED_MY_PRAY("공유~", "다른 사람이 내 기도 제목을 공유 받았을 때") + ; + private final String title; + private final String body; +} diff --git a/src/main/java/com/uspray/uspray/config/SecurityConfig.java b/src/main/java/com/uspray/uspray/config/SecurityConfig.java index 6723c105..2086c3d8 100644 --- a/src/main/java/com/uspray/uspray/config/SecurityConfig.java +++ b/src/main/java/com/uspray/uspray/config/SecurityConfig.java @@ -50,6 +50,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .antMatchers("/auth/**").permitAll() .antMatchers("/swagger-ui/**", "/v3/api-docs/**", "/api-docs/**", "/swagger-ui.html").permitAll() .antMatchers("/sms/**").permitAll() + .antMatchers("/admin/**").permitAll() .anyRequest().authenticated() .and() .apply(new JwtSecurityConfig(tokenProvider)); diff --git a/src/main/java/com/uspray/uspray/controller/AuthController.java b/src/main/java/com/uspray/uspray/controller/AuthController.java index 6453c4b6..078d2085 100644 --- a/src/main/java/com/uspray/uspray/controller/AuthController.java +++ b/src/main/java/com/uspray/uspray/controller/AuthController.java @@ -4,7 +4,6 @@ import com.uspray.uspray.DTO.auth.request.FindPwDto; import com.uspray.uspray.DTO.auth.request.MemberLoginRequestDto; import com.uspray.uspray.DTO.auth.request.MemberRequestDto; -import com.uspray.uspray.DTO.auth.request.TokenRequestDto; import com.uspray.uspray.DTO.ApiResponseDto; import com.uspray.uspray.DTO.auth.TokenDto; import com.uspray.uspray.exception.SuccessStatus; diff --git a/src/main/java/com/uspray/uspray/controller/FCMController.java b/src/main/java/com/uspray/uspray/controller/FCMController.java new file mode 100644 index 00000000..d99a0b2d --- /dev/null +++ b/src/main/java/com/uspray/uspray/controller/FCMController.java @@ -0,0 +1,29 @@ +package com.uspray.uspray.controller; + +import com.uspray.uspray.DTO.ApiResponseDto; +import com.uspray.uspray.DTO.notification.FCMNotificationRequestDto; +import com.uspray.uspray.exception.SuccessStatus; +import com.uspray.uspray.service.FCMNotificationService; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class FCMController { + + private final FCMNotificationService fcmNotificationService; + + @PostMapping("/admin/send/push") + public ApiResponseDto pushMessage(@RequestBody FCMNotificationRequestDto requestDto) throws IOException { + + fcmNotificationService.sendMessageTo( + requestDto.getToken(), + requestDto.getTitle(), + requestDto.getBody()); + return ApiResponseDto.success(SuccessStatus.PUSH_SUCCESS); + } + +} diff --git a/src/main/java/com/uspray/uspray/controller/MemberController.java b/src/main/java/com/uspray/uspray/controller/MemberController.java index ccc9d4f6..eb28de47 100644 --- a/src/main/java/com/uspray/uspray/controller/MemberController.java +++ b/src/main/java/com/uspray/uspray/controller/MemberController.java @@ -1,9 +1,11 @@ package com.uspray.uspray.controller; +import com.uspray.uspray.DTO.notification.NotificationAgreeDto; import com.uspray.uspray.DTO.ApiResponseDto; import com.uspray.uspray.exception.SuccessStatus; import com.uspray.uspray.service.MemberService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -12,6 +14,7 @@ import org.springframework.security.core.userdetails.User; import org.springframework.web.bind.annotation.PathVariable; 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; @@ -24,11 +27,21 @@ public class MemberController { private final MemberService memberService; - @Operation(summary = "전화번호 변경") - @PostMapping("/{changePhone}") - public ApiResponseDto changePhone(@AuthenticationPrincipal User user, - @Schema(example = "01046518879") @PathVariable("changePhone") String changePhone) { - memberService.changePhone(user.getUsername(), changePhone); - return ApiResponseDto.success(SuccessStatus.CHANGE_PHONE_SUCCESS); - } + @Operation(summary = "전화번호 변경") + @PostMapping("/{changePhone}") + public ApiResponseDto changePhone( + @Parameter(hidden = true) @AuthenticationPrincipal User user, + @Schema(example = "01046518879") @PathVariable("changePhone") String changePhone) { + memberService.changePhone(user.getUsername(), changePhone); + return ApiResponseDto.success(SuccessStatus.CHANGE_PHONE_SUCCESS); + } + + @Operation(summary = "알림 On/Off") + @PostMapping("/nodification-setting") + public ApiResponseDto setNotificationAgree( + @Parameter(hidden = true) @AuthenticationPrincipal User user, + @RequestBody NotificationAgreeDto notificationAgreeDto) { + memberService.changeNotificationAgree(user.getUsername(), notificationAgreeDto); + return ApiResponseDto.success(SuccessStatus.CHANGE_PUSH_AGREE_SUCCESS); + } } diff --git a/src/main/java/com/uspray/uspray/domain/FCMMessage.java b/src/main/java/com/uspray/uspray/domain/FCMMessage.java new file mode 100644 index 00000000..3766a9ac --- /dev/null +++ b/src/main/java/com/uspray/uspray/domain/FCMMessage.java @@ -0,0 +1,30 @@ +package com.uspray.uspray.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Builder +@AllArgsConstructor +@Getter +public class FCMMessage { + private boolean validate_only; + private Message message; + + @Builder + @AllArgsConstructor + @Getter + public static class Message { + private Notification notification; // 모든 mobile os를 아우를수 있는 Notification + private String token; // 특정 device에 알림을 보내기위해 사용 + } + + @Builder + @AllArgsConstructor + @Getter + public static class Notification { + private String title; + private String body; + private String image; + } +} diff --git a/src/main/java/com/uspray/uspray/domain/Member.java b/src/main/java/com/uspray/uspray/domain/Member.java index 451f0064..5163199d 100644 --- a/src/main/java/com/uspray/uspray/domain/Member.java +++ b/src/main/java/com/uspray/uspray/domain/Member.java @@ -1,5 +1,6 @@ package com.uspray.uspray.domain; +import com.uspray.uspray.DTO.notification.NotificationAgreeDto; import com.uspray.uspray.Enums.Authority; import com.uspray.uspray.common.domain.AuditingTimeEntity; import javax.persistence.Column; @@ -35,13 +36,22 @@ public class Member extends AuditingTimeEntity { private String phone; private String birth; private String gender; + private String firebaseToken; + private Boolean firstNotiAgree = true; + private Boolean secondNotiAgree= true; + private Boolean thirdNotiAgree = true; + private final Boolean deleted = false; @Enumerated(EnumType.STRING) private Authority authority; + public void changeFirebaseToken(String firebaseToken) { + this.firebaseToken = firebaseToken; + } + public void changePhone(String phone) { this.phone = phone; } @@ -62,4 +72,19 @@ public Member(String userId, String password, String name, String phone, String this.authority = authority; } + public void changeNotificationSetting(NotificationAgreeDto notificationAgreeDto) { + switch (notificationAgreeDto.getNotificationType()) { + case PRAY_TIME: + this.firstNotiAgree = notificationAgreeDto.getAgree(); + break; + case PRAY_FOR_ME: + this.secondNotiAgree = notificationAgreeDto.getAgree(); + break; + case SHARED_MY_PRAY: + this.thirdNotiAgree = notificationAgreeDto.getAgree(); + break; + default: + break; + } + } } diff --git a/src/main/java/com/uspray/uspray/exception/SuccessStatus.java b/src/main/java/com/uspray/uspray/exception/SuccessStatus.java index de37ea50..fb8bb615 100644 --- a/src/main/java/com/uspray/uspray/exception/SuccessStatus.java +++ b/src/main/java/com/uspray/uspray/exception/SuccessStatus.java @@ -9,7 +9,6 @@ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public enum SuccessStatus { - /** * 200 OK */ @@ -24,6 +23,8 @@ public enum SuccessStatus { CHECK_USER_ID_SUCCESS(HttpStatus.OK, "사용 가능한 아이디입니다."), UPDATE_PRAY_SUCCESS(HttpStatus.OK, "기도제목 수정에 성공했습니다."), REISSUE_SUCCESS(HttpStatus.OK, "토큰 재발급에 성공했습니다."), + PUSH_SUCCESS(HttpStatus.OK, "푸쉬 알림을 성공적으로 전송했습니다."), + CHANGE_PUSH_AGREE_SUCCESS(HttpStatus.OK, "푸쉬 알림 설정을 성공적으로 변경했습니다."), /* @@ -41,4 +42,5 @@ public enum SuccessStatus { private final HttpStatus httpStatus; private final String message; + } diff --git a/src/main/java/com/uspray/uspray/infrastructure/MemberRepository.java b/src/main/java/com/uspray/uspray/infrastructure/MemberRepository.java index 33c3869d..7619e161 100644 --- a/src/main/java/com/uspray/uspray/infrastructure/MemberRepository.java +++ b/src/main/java/com/uspray/uspray/infrastructure/MemberRepository.java @@ -12,12 +12,8 @@ public interface MemberRepository extends JpaRepository { Optional findByUserId(String userId); - Optional findByPhone(String phone); - boolean existsByUserId(String userId); - boolean existsByPhone(String phone); - Member findByNameAndPhone(String name, String phone); Member findByNameAndPhoneAndUserId(String name, String phone, String userId); diff --git a/src/main/java/com/uspray/uspray/infrastructure/query/MemberQueryRepository.java b/src/main/java/com/uspray/uspray/infrastructure/query/MemberQueryRepository.java new file mode 100644 index 00000000..dbcac2db --- /dev/null +++ b/src/main/java/com/uspray/uspray/infrastructure/query/MemberQueryRepository.java @@ -0,0 +1,16 @@ +package com.uspray.uspray.infrastructure.query; + +import com.uspray.uspray.domain.Member; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface MemberQueryRepository extends JpaRepository { + + @Query("select m.firebaseToken from Member m where m.firstNotiAgree = :agree") + List getDeviceTokensByFirstNotiAgree(@Param("agree") Boolean agree); + +} diff --git a/src/main/java/com/uspray/uspray/service/AuthService.java b/src/main/java/com/uspray/uspray/service/AuthService.java index 08834b83..30e66949 100644 --- a/src/main/java/com/uspray/uspray/service/AuthService.java +++ b/src/main/java/com/uspray/uspray/service/AuthService.java @@ -5,12 +5,10 @@ import com.uspray.uspray.DTO.auth.request.FindPwDto; import com.uspray.uspray.DTO.auth.request.MemberLoginRequestDto; import com.uspray.uspray.DTO.auth.request.MemberRequestDto; -import com.uspray.uspray.DTO.auth.request.TokenRequestDto; import com.uspray.uspray.DTO.auth.response.MemberResponseDto; import com.uspray.uspray.domain.Member; import com.uspray.uspray.exception.ErrorStatus; import com.uspray.uspray.exception.model.ExistIdException; -import com.uspray.uspray.exception.model.TokenNotValidException; import com.uspray.uspray.infrastructure.MemberRepository; import com.uspray.uspray.jwt.TokenProvider; import java.util.concurrent.TimeUnit; @@ -37,8 +35,7 @@ public class AuthService { public MemberResponseDto signup(MemberRequestDto memberRequestDto) { // 핸드폰번호가 존재하거나 아이디가 존재하면 에러 // 핸드폰 번호 또는 아이디가 이미 존재하는지 확인 - if (memberRepository.existsByUserId(memberRequestDto.getUserId()) - || memberRepository.existsByPhone(memberRequestDto.getPhone())) { + if (memberRepository.existsByUserId(memberRequestDto.getUserId())) { throw new RuntimeException("이미 가입되어 있는 유저입니다"); } diff --git a/src/main/java/com/uspray/uspray/service/FCMNotificationService.java b/src/main/java/com/uspray/uspray/service/FCMNotificationService.java new file mode 100644 index 00000000..df0beffd --- /dev/null +++ b/src/main/java/com/uspray/uspray/service/FCMNotificationService.java @@ -0,0 +1,70 @@ +package com.uspray.uspray.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.net.HttpHeaders; +import com.uspray.uspray.domain.FCMMessage; +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +@Slf4j +public class FCMNotificationService { + + private final ObjectMapper objectMapper; + + public void sendMessageTo(String targetToken, String title, String body) throws IOException { + String message = makeMessage(targetToken, title, body); + + OkHttpClient client = new OkHttpClient(); + RequestBody requestBody = RequestBody.create(message, MediaType.get("application/json; charset=utf-8")); + String API_URL = "https://fcm.googleapis.com/v1/projects/prayhelper-8563a/messages:send"; + Request request = new Request.Builder() + .url(API_URL) + .post(requestBody) + .addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + getAccessToken()) + .addHeader(HttpHeaders.CONTENT_TYPE, "application/json; UTF-8") + .build(); + + Response response = client.newCall(request).execute(); + + log.info(Objects.requireNonNull(response.body()).string()); + } + + private String makeMessage(String targetToken, String title, String body) throws JsonProcessingException { + FCMMessage fcmMessage = FCMMessage.builder() + .message(FCMMessage.Message.builder() + .token(targetToken) + .notification(FCMMessage.Notification.builder() + .title(title) + .body(body) + .image(null) + .build() + ) + .build() + ) + .validate_only(false) + .build(); + return objectMapper.writeValueAsString(fcmMessage); + } + + private String getAccessToken() throws IOException { + String firebaseConfigPath = "/firebase/service-account-file.json"; + + GoogleCredentials googleCredentials = GoogleCredentials + .fromStream(new ClassPathResource(firebaseConfigPath).getInputStream()) + .createScoped(List.of("https://www.googleapis.com/auth/cloud-platform")); + + googleCredentials.refreshIfExpired(); + return googleCredentials.getAccessToken().getTokenValue(); + } + +} diff --git a/src/main/java/com/uspray/uspray/service/MemberService.java b/src/main/java/com/uspray/uspray/service/MemberService.java index 434f4ebe..a18bf0e7 100644 --- a/src/main/java/com/uspray/uspray/service/MemberService.java +++ b/src/main/java/com/uspray/uspray/service/MemberService.java @@ -1,5 +1,6 @@ package com.uspray.uspray.service; +import com.uspray.uspray.DTO.notification.NotificationAgreeDto; import com.uspray.uspray.infrastructure.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -9,10 +10,15 @@ @RequiredArgsConstructor public class MemberService { - private MemberRepository memberRepository; + private final MemberRepository memberRepository; @Transactional public void changePhone(String userId, String phone) { memberRepository.getMemberByUserId(userId).changePhone(phone); } + + @Transactional + public void changeNotificationAgree(String userId, NotificationAgreeDto notificationAgreeDto) { + memberRepository.getMemberByUserId(userId).changeNotificationSetting(notificationAgreeDto); + } } diff --git a/src/main/java/com/uspray/uspray/service/SchedulerService.java b/src/main/java/com/uspray/uspray/service/SchedulerService.java new file mode 100644 index 00000000..b0d75d73 --- /dev/null +++ b/src/main/java/com/uspray/uspray/service/SchedulerService.java @@ -0,0 +1,26 @@ +package com.uspray.uspray.service; + +import com.uspray.uspray.Enums.NotificationType; +import com.uspray.uspray.infrastructure.query.MemberQueryRepository; +import java.io.IOException; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class SchedulerService { + + private final MemberQueryRepository memberQueryRepository; + private final FCMNotificationService fcmNotificationService; + + @Scheduled(cron = "0 0 8 * * *") + public void pushPrayNotification() throws IOException { + List deviceTokens = memberQueryRepository.getDeviceTokensByFirstNotiAgree( + true); + for (String device : deviceTokens) { + fcmNotificationService.sendMessageTo(device, NotificationType.PRAY_TIME.getTitle(), NotificationType.PRAY_TIME.getBody()); + } + } +}