diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 0c0060a..013a83c 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -34,6 +34,11 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew\ + - name: Setup Firebase service key + run: | + mkdir -p src/main/resources/firebase + echo ${{ secrets.FIREBASE_SERVICE_KEY_BASE64_ENCODE }} | base64 -d > src/main/resources/firebase/petlog-firebase-key.json + - name: Build with gradle run: ./gradlew bootJar -Pspring.profiles.active=dev --info diff --git a/.gitignore b/.gitignore index f821ca3..e7f6541 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ out/ ### docker data ### /docker/data/mysql/petlog_local + +# firebase +src/main/resources/firebase diff --git a/build.gradle b/build.gradle index 0fb33e9..1ba72f3 100644 --- a/build.gradle +++ b/build.gradle @@ -55,6 +55,9 @@ dependencies { //AWS S3 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + // Firebase + implementation 'com.google.firebase:firebase-admin:9.4.3' + // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/src/main/java/com/petlog/PetlogApplication.java b/src/main/java/com/petlog/PetlogApplication.java index 650793d..72f2069 100644 --- a/src/main/java/com/petlog/PetlogApplication.java +++ b/src/main/java/com/petlog/PetlogApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling @SpringBootApplication public class PetlogApplication { diff --git a/src/main/java/com/petlog/common/config/SecurityConfig.java b/src/main/java/com/petlog/auth/config/SecurityConfig.java similarity index 93% rename from src/main/java/com/petlog/common/config/SecurityConfig.java rename to src/main/java/com/petlog/auth/config/SecurityConfig.java index 25a3462..85fd2fd 100644 --- a/src/main/java/com/petlog/common/config/SecurityConfig.java +++ b/src/main/java/com/petlog/auth/config/SecurityConfig.java @@ -1,7 +1,7 @@ -package com.petlog.common.config; +package com.petlog.auth.config; -import com.petlog.common.config.jwt.TokenProvider; -import com.petlog.common.config.jwt.filter.TokenAuthenticationFilter; +import com.petlog.auth.jwt.TokenProvider; +import com.petlog.auth.jwt.filter.TokenAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/petlog/common/config/jwt/JwtProperties.java b/src/main/java/com/petlog/auth/jwt/JwtProperties.java similarity index 89% rename from src/main/java/com/petlog/common/config/jwt/JwtProperties.java rename to src/main/java/com/petlog/auth/jwt/JwtProperties.java index 1fc16e0..ac21d57 100644 --- a/src/main/java/com/petlog/common/config/jwt/JwtProperties.java +++ b/src/main/java/com/petlog/auth/jwt/JwtProperties.java @@ -1,4 +1,4 @@ -package com.petlog.common.config.jwt; +package com.petlog.auth.jwt; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/com/petlog/common/config/jwt/TokenProvider.java b/src/main/java/com/petlog/auth/jwt/TokenProvider.java similarity index 98% rename from src/main/java/com/petlog/common/config/jwt/TokenProvider.java rename to src/main/java/com/petlog/auth/jwt/TokenProvider.java index cc22aec..bf970fd 100644 --- a/src/main/java/com/petlog/common/config/jwt/TokenProvider.java +++ b/src/main/java/com/petlog/auth/jwt/TokenProvider.java @@ -1,4 +1,4 @@ -package com.petlog.common.config.jwt; +package com.petlog.auth.jwt; import com.petlog.member.entity.Member; import io.jsonwebtoken.Claims; diff --git a/src/main/java/com/petlog/common/config/jwt/TokenType.java b/src/main/java/com/petlog/auth/jwt/TokenType.java similarity index 65% rename from src/main/java/com/petlog/common/config/jwt/TokenType.java rename to src/main/java/com/petlog/auth/jwt/TokenType.java index 23262bc..6279319 100644 --- a/src/main/java/com/petlog/common/config/jwt/TokenType.java +++ b/src/main/java/com/petlog/auth/jwt/TokenType.java @@ -1,4 +1,4 @@ -package com.petlog.common.config.jwt; +package com.petlog.auth.jwt; public enum TokenType { diff --git a/src/main/java/com/petlog/common/config/jwt/filter/TokenAuthenticationFilter.java b/src/main/java/com/petlog/auth/jwt/filter/TokenAuthenticationFilter.java similarity index 94% rename from src/main/java/com/petlog/common/config/jwt/filter/TokenAuthenticationFilter.java rename to src/main/java/com/petlog/auth/jwt/filter/TokenAuthenticationFilter.java index d9bb421..e621d9f 100644 --- a/src/main/java/com/petlog/common/config/jwt/filter/TokenAuthenticationFilter.java +++ b/src/main/java/com/petlog/auth/jwt/filter/TokenAuthenticationFilter.java @@ -1,6 +1,6 @@ -package com.petlog.common.config.jwt.filter; +package com.petlog.auth.jwt.filter; -import com.petlog.common.config.jwt.TokenProvider; +import com.petlog.auth.jwt.TokenProvider; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/com/petlog/auth/resolver/AuthenticatedArgumentResolver.java b/src/main/java/com/petlog/auth/resolver/AuthenticatedArgumentResolver.java index 3101f7d..662aead 100644 --- a/src/main/java/com/petlog/auth/resolver/AuthenticatedArgumentResolver.java +++ b/src/main/java/com/petlog/auth/resolver/AuthenticatedArgumentResolver.java @@ -1,6 +1,6 @@ package com.petlog.auth.resolver; -import com.petlog.common.config.jwt.TokenProvider; +import com.petlog.auth.jwt.TokenProvider; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/petlog/auth/service/TokenService.java b/src/main/java/com/petlog/auth/service/TokenService.java index 2edeaf1..ecbab6c 100644 --- a/src/main/java/com/petlog/auth/service/TokenService.java +++ b/src/main/java/com/petlog/auth/service/TokenService.java @@ -2,7 +2,7 @@ import com.petlog.auth.entity.RefreshToken; import com.petlog.auth.repository.RefreshTokenRepository; -import com.petlog.common.config.jwt.TokenProvider; +import com.petlog.auth.jwt.TokenProvider; import com.petlog.member.entity.Member; import com.petlog.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/petlog/docs/NotificationControllerDocs.java b/src/main/java/com/petlog/docs/NotificationControllerDocs.java new file mode 100644 index 0000000..8df4891 --- /dev/null +++ b/src/main/java/com/petlog/docs/NotificationControllerDocs.java @@ -0,0 +1,22 @@ +package com.petlog.docs; + +import com.petlog.auth.resolver.Authenticated; +import com.petlog.common.response.ApiResponse; +import com.petlog.notification.controller.dto.request.DeleteNotificationTokenRequestDto; +import com.petlog.notification.controller.dto.request.SaveNotificationTokenRequestDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "푸시 알림 API") +public interface NotificationControllerDocs { + + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "201", description = "푸시 알림 토큰 저장에 성공하였습니다.") + @Operation(summary = "푸시 알림 토큰 저장 API") + ResponseEntity> saveNotificationToken(@Authenticated final Long memberId, @RequestBody final SaveNotificationTokenRequestDto request); + + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "푸시 알림 토큰 삭제에 성공하였습니다.") + @Operation(summary = "푸시 알림 토큰 삭제 API") + ResponseEntity> deleteNotificationToken(@Authenticated final Long memberId, @RequestBody final DeleteNotificationTokenRequestDto request); +} diff --git a/src/main/java/com/petlog/notification/config/FirebaseConfig.java b/src/main/java/com/petlog/notification/config/FirebaseConfig.java new file mode 100644 index 0000000..bf390b4 --- /dev/null +++ b/src/main/java/com/petlog/notification/config/FirebaseConfig.java @@ -0,0 +1,40 @@ +package com.petlog.notification.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +import javax.annotation.PostConstruct; +import java.io.IOException; +import java.io.InputStream; + +@Slf4j +@Configuration +public class FirebaseConfig { + + @Value("${firebase.petlog-firebase-key.path}") + private String SERVICE_ACCOUNT_PATH; + + @PostConstruct + public void init() throws IOException { + try { + final InputStream serviceAccount = new ClassPathResource(SERVICE_ACCOUNT_PATH).getInputStream(); + final FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(serviceAccount)) + .build(); + + if (FirebaseApp.getApps().isEmpty()) { + FirebaseApp.initializeApp(options); + } + + log.info("FirebaseApp 초기화 성공 - {}", SERVICE_ACCOUNT_PATH); + } catch (final Exception e) { + log.error("FirebaseApp 초기화 실패", e); + throw e; + } + } +} diff --git a/src/main/java/com/petlog/notification/controller/NotificationController.java b/src/main/java/com/petlog/notification/controller/NotificationController.java new file mode 100644 index 0000000..0231261 --- /dev/null +++ b/src/main/java/com/petlog/notification/controller/NotificationController.java @@ -0,0 +1,50 @@ +package com.petlog.notification.controller; + +import com.petlog.auth.resolver.Authenticated; +import com.petlog.common.response.ApiResponse; +import com.petlog.docs.NotificationControllerDocs; +import com.petlog.notification.controller.dto.request.DeleteNotificationTokenRequestDto; +import com.petlog.notification.controller.dto.request.SaveNotificationTokenRequestDto; +import com.petlog.notification.service.NotificationService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +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; + +import static com.petlog.notification.controller.NotificationSuccessCode.DELETE_NOTIFICATION_TOKEN; +import static com.petlog.notification.controller.NotificationSuccessCode.SAVE_NOTIFICATION_TOKEN; + +@RequiredArgsConstructor +@RequestMapping("api/notification") +@RestController +public class NotificationController implements NotificationControllerDocs { + + private final NotificationService notificationService; + + @PostMapping("/token") + public ResponseEntity> saveNotificationToken( + @Authenticated final Long memberId, + @RequestBody final SaveNotificationTokenRequestDto request + ) { + notificationService.saveNotificationToken(memberId, request.token()); + + return ResponseEntity.ok( + ApiResponse.success(SAVE_NOTIFICATION_TOKEN) + ); + } + + @DeleteMapping("/token") + public ResponseEntity> deleteNotificationToken( + @Authenticated final Long memberId, + @RequestBody final DeleteNotificationTokenRequestDto request + ) { + notificationService.deleteNotificationToken(memberId, request.token()); + + return ResponseEntity.ok( + ApiResponse.success(DELETE_NOTIFICATION_TOKEN) + ); + } +} diff --git a/src/main/java/com/petlog/notification/controller/NotificationSuccessCode.java b/src/main/java/com/petlog/notification/controller/NotificationSuccessCode.java new file mode 100644 index 0000000..e5f454b --- /dev/null +++ b/src/main/java/com/petlog/notification/controller/NotificationSuccessCode.java @@ -0,0 +1,18 @@ +package com.petlog.notification.controller; + +import com.petlog.common.response.SuccessCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum NotificationSuccessCode implements SuccessCode { + + SAVE_NOTIFICATION_TOKEN(HttpStatus.CREATED.value(), "푸시 알림 토큰 저장에 성공하였습니다."), + DELETE_NOTIFICATION_TOKEN(HttpStatus.OK.value(), "푸시 알림 토큰 삭제에 성공하였습니다."), + ; + + private final int value; + private final String message; +} diff --git a/src/main/java/com/petlog/notification/controller/dto/request/DeleteNotificationTokenRequestDto.java b/src/main/java/com/petlog/notification/controller/dto/request/DeleteNotificationTokenRequestDto.java new file mode 100644 index 0000000..7a8a8ac --- /dev/null +++ b/src/main/java/com/petlog/notification/controller/dto/request/DeleteNotificationTokenRequestDto.java @@ -0,0 +1,8 @@ +package com.petlog.notification.controller.dto.request; + +public record DeleteNotificationTokenRequestDto( + + String token + +) { +} diff --git a/src/main/java/com/petlog/notification/controller/dto/request/SaveNotificationTokenRequestDto.java b/src/main/java/com/petlog/notification/controller/dto/request/SaveNotificationTokenRequestDto.java new file mode 100644 index 0000000..eeea72a --- /dev/null +++ b/src/main/java/com/petlog/notification/controller/dto/request/SaveNotificationTokenRequestDto.java @@ -0,0 +1,8 @@ +package com.petlog.notification.controller.dto.request; + +public record SaveNotificationTokenRequestDto( + + String token + +) { +} diff --git a/src/main/java/com/petlog/notification/entity/NotificationToken.java b/src/main/java/com/petlog/notification/entity/NotificationToken.java new file mode 100644 index 0000000..c9a0320 --- /dev/null +++ b/src/main/java/com/petlog/notification/entity/NotificationToken.java @@ -0,0 +1,39 @@ +package com.petlog.notification.entity; + +import com.petlog.common.entity.BaseEntity; +import com.petlog.member.entity.Member; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "notification_token") +@Entity +public class NotificationToken extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false, updatable = false) + private Member member; + + @Column(name = "value", length = 500, nullable = false, unique = true) + private String value; + + public NotificationToken(final Member member, final String value) { + this.member = member; + this.value = value; + } +} diff --git a/src/main/java/com/petlog/notification/repository/NotificationTokenRepository.java b/src/main/java/com/petlog/notification/repository/NotificationTokenRepository.java new file mode 100644 index 0000000..1ca51f0 --- /dev/null +++ b/src/main/java/com/petlog/notification/repository/NotificationTokenRepository.java @@ -0,0 +1,19 @@ +package com.petlog.notification.repository; + +import com.petlog.member.entity.Member; +import com.petlog.notification.entity.NotificationToken; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface NotificationTokenRepository extends JpaRepository { + + List findAllByMember_Id(final Long memberId); + + void deleteAllByValue(final String value); + + boolean existsByValue(final String token); + + Optional findByMemberAndValue(final Member member, final String value); +} diff --git a/src/main/java/com/petlog/notification/sender/FcmNotificationSender.java b/src/main/java/com/petlog/notification/sender/FcmNotificationSender.java new file mode 100644 index 0000000..0eabf10 --- /dev/null +++ b/src/main/java/com/petlog/notification/sender/FcmNotificationSender.java @@ -0,0 +1,33 @@ +package com.petlog.notification.sender; + +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.petlog.notification.sender.dto.FcmNotificationRequestDto; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class FcmNotificationSender { + + public void send(final FcmNotificationRequestDto request) { + try { + final String response = FirebaseMessaging.getInstance().send(request.convertFcmMessage()); + log.info("푸시 알림 전송 완료 - {}", response); + } + catch (final FirebaseMessagingException e) { + log.error("푸시 알림 전송에 실패하였습니다.", e); + handleFcmException(e.getMessage()); + } + } + + private void handleFcmException(final String errorResponse) { + if (checkInvalidFcmTokenResponse(errorResponse)) { + throw new IllegalArgumentException("유효하지 않은 푸시 알림 토큰입니다."); + } + } + + private boolean checkInvalidFcmTokenResponse(final String errorResponse) { + return errorResponse.contains("The registration token is not a valid FCM registration token"); + } +} diff --git a/src/main/java/com/petlog/notification/sender/dto/FcmNotificationRequestDto.java b/src/main/java/com/petlog/notification/sender/dto/FcmNotificationRequestDto.java new file mode 100644 index 0000000..946e703 --- /dev/null +++ b/src/main/java/com/petlog/notification/sender/dto/FcmNotificationRequestDto.java @@ -0,0 +1,49 @@ +package com.petlog.notification.sender.dto; + +import com.google.firebase.messaging.AndroidConfig; +import com.google.firebase.messaging.ApnsConfig; +import com.google.firebase.messaging.Aps; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; + +public record FcmNotificationRequestDto( + + String fcmToken, + String title, + String body, + String type + +) { + + public Message convertFcmMessage() { + return Message.builder() + .setNotification(createNotification()) + .setToken(fcmToken) + .setApnsConfig(createApnsConfig()) + .setAndroidConfig(createAndroidConfig()) + .putData("type", type) + .build(); + } + + private ApnsConfig createApnsConfig() { + return ApnsConfig.builder() + .setAps( + Aps.builder() + .setContentAvailable(true) + .build()) + .build(); + } + + private static AndroidConfig createAndroidConfig() { + return AndroidConfig.builder() + .setPriority(AndroidConfig.Priority.HIGH) + .build(); + } + + private Notification createNotification() { + return Notification.builder() + .setTitle(title) + .setBody(body) + .build(); + } +} diff --git a/src/main/java/com/petlog/notification/service/NotificationService.java b/src/main/java/com/petlog/notification/service/NotificationService.java new file mode 100644 index 0000000..3ad3bd7 --- /dev/null +++ b/src/main/java/com/petlog/notification/service/NotificationService.java @@ -0,0 +1,87 @@ +package com.petlog.notification.service; + +import com.petlog.member.entity.Member; +import com.petlog.member.repository.MemberRepository; +import com.petlog.notification.entity.NotificationToken; +import com.petlog.notification.repository.NotificationTokenRepository; +import com.petlog.notification.sender.FcmNotificationSender; +import com.petlog.notification.sender.dto.FcmNotificationRequestDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Service +public class NotificationService { + + private final FcmNotificationSender fcmNotificationSender; + private final NotificationTokenRepository notificationTokenRepository; + private final MemberRepository memberRepository; + + @Transactional + public void sendNotification( + final Long addresseeId, + final String title, + final String body, + final String type + ) { + notificationTokenRepository.findAllByMember_Id(addresseeId).forEach( + notificationToken -> sendNotification(notificationToken, title, body, type)); + } + + private void sendNotification( + final NotificationToken notificationToken, + final String title, + final String body, + final String type + ) { + try { + final FcmNotificationRequestDto fcmNotificationRequest = new FcmNotificationRequestDto(notificationToken.getValue(), title, body, type); + fcmNotificationSender.send(fcmNotificationRequest); + } + catch (final IllegalArgumentException e) { + notificationTokenRepository.deleteAllByValue(notificationToken.getValue()); + log.info("유효하지 않은 토큰 제거 - {}", notificationToken.getValue()); + } + } + + @Transactional + public void saveNotificationToken(final Long memberId, final String notificationToken) { + validateNotificationTokenIsNullOrEmpty(notificationToken); + + final Member member = getMember(memberId); + if (notificationTokenRepository.existsByValue(notificationToken)) { + log.trace("이미 존재하는 푸시 알림 토큰입니다."); + return; + } + + final NotificationToken notificationTokenJpaEntity = new NotificationToken(member, notificationToken); + notificationTokenRepository.save(notificationTokenJpaEntity); + log.trace("푸시 알림 토큰 저장 - {}", notificationToken); + } + + private void validateNotificationTokenIsNullOrEmpty(final String token) { + if (token == null || token.isEmpty()) { + log.info("입력된 알림 토큰 값이 null 혹은 공백 - {}", token); + throw new IllegalArgumentException("알림 토큰 값은 공백일 수 없습니다."); + } + } + + private Member getMember(final Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + } + + @Transactional + public void deleteNotificationToken(final Long memberId, final String notificationToken) { + validateNotificationTokenIsNullOrEmpty(notificationToken); + + final Member member = getMember(memberId); + notificationTokenRepository.findByMemberAndValue(member, notificationToken) + .ifPresent(notificationTokenJpaEntity -> { + notificationTokenRepository.delete(notificationTokenJpaEntity); + log.info("푸시 알림 토큰 제거 - {}", notificationToken);}); + } +} diff --git a/src/main/java/com/petlog/pet/entity/FeedingDailyRecord.java b/src/main/java/com/petlog/pet/entity/FeedingDailyRecord.java index ad18fd7..b9c4df9 100644 --- a/src/main/java/com/petlog/pet/entity/FeedingDailyRecord.java +++ b/src/main/java/com/petlog/pet/entity/FeedingDailyRecord.java @@ -41,6 +41,18 @@ public class FeedingDailyRecord extends BaseEntity { @Column(name = "memo", length = 200, nullable = true) private String memo; + public FeedingDailyRecord( + final PetProfile petProfile, + final Member member, + final LocalDateTime time, + final String memo + ) { + this.petProfile = petProfile; + this.member = member; + this.time = time; + this.memo = memo; + } + public FeedingDailyRecord( final PetProfile petProfile, final Member member, diff --git a/src/main/java/com/petlog/pet/entity/WateringDailyRecord.java b/src/main/java/com/petlog/pet/entity/WateringDailyRecord.java index 3836a11..6922230 100644 --- a/src/main/java/com/petlog/pet/entity/WateringDailyRecord.java +++ b/src/main/java/com/petlog/pet/entity/WateringDailyRecord.java @@ -41,6 +41,18 @@ public class WateringDailyRecord extends BaseEntity { @Column(name = "memo", length = 200, nullable = true) private String memo; + public WateringDailyRecord( + final PetProfile petProfile, + final Member member, + final LocalDateTime time, + final String memo + ) { + this.petProfile = petProfile; + this.member = member; + this.time = time; + this.memo = memo; + } + public WateringDailyRecord( final PetProfile petProfile, final Member member, diff --git a/src/main/java/com/petlog/pet/scheduling/PetInfoScheduling.java b/src/main/java/com/petlog/pet/scheduling/PetInfoScheduling.java new file mode 100644 index 0000000..6b4cd21 --- /dev/null +++ b/src/main/java/com/petlog/pet/scheduling/PetInfoScheduling.java @@ -0,0 +1,96 @@ +package com.petlog.pet.scheduling; + +import com.petlog.notification.service.NotificationService; +import com.petlog.pet.entity.FeedingDailyRecord; +import com.petlog.pet.entity.PetProfile; +import com.petlog.pet.entity.WateringDailyRecord; +import com.petlog.pet.repository.FeedingDailyRecordRepository; +import com.petlog.pet.repository.PetProfileRepository; +import com.petlog.pet.repository.WateringDailyRecordRepository; +import com.petlog.petgroup.entity.PetGroup; +import com.petlog.petgroup.entity.PetGroupMember; +import com.petlog.petgroup.repository.PetGroupMemberRepository; +import com.petlog.petgroup.repository.PetGroupRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +@RequiredArgsConstructor +@Component +public class PetInfoScheduling { + + private final NotificationService notificationService; + private final PetProfileRepository petProfileRepository; + private final PetGroupRepository petGroupRepository; + private final PetGroupMemberRepository petGroupMemberRepository; + private final FeedingDailyRecordRepository feedingDailyRecordRepository; + private final WateringDailyRecordRepository wateringDailyRecordRepository; + + @Scheduled(cron = "0 0 */1 * * *") + public void checkFeedingCycleAndSendNotification() { + final List petGroups = petGroupRepository.findAll(); + + for(final PetGroup petGroup : petGroups) { + final List groupMembers = getPetGroupMembers(petGroup); + + if (groupMembers.isEmpty()) { + continue; + } + + final PetProfile petProfile = petProfileRepository.findByPetGroupId(petGroup.getId()); + final FeedingDailyRecord feedingDailyRecord = feedingDailyRecordRepository.findTopByPetProfileOrderByTimeDesc(petProfile); + final WateringDailyRecord wateringDailyRecord = wateringDailyRecordRepository.findTopByPetProfileOrderByTimeDesc(petProfile); + + final long hoursSinceLastFeeding = Duration + .between(feedingDailyRecord.getTime(), LocalDateTime.now()) + .toHours(); + + final long hoursSinceLastWatering = Duration + .between(wateringDailyRecord.getTime(), LocalDateTime.now()) + .toHours(); + + if(hoursSinceLastFeeding >= petProfile.getFeedingCycle()) { + sendFeedingNotificationToAllGroupMembers(groupMembers, petProfile, hoursSinceLastFeeding); + } + + if(hoursSinceLastWatering >= petProfile.getWateringCycle()) { + sendWateringNotificationToAllGroupMembers(groupMembers, petProfile, hoursSinceLastWatering); + } + } + } + + private List getPetGroupMembers(final PetGroup petGroup) { + return petGroupMemberRepository.findAllByPetGroup(petGroup) + .orElse(List.of()); + } + + private void sendFeedingNotificationToAllGroupMembers(final List groupMembers, final PetProfile petProfile, final Long hoursSinceLastFeeding) { + for (final PetGroupMember groupMember : groupMembers) { + final Long groupMemberId = groupMember.getMember().getId(); + + notificationService.sendNotification( + groupMemberId, + petProfile.getName() + "이에게 밥을 줄 시간이에요!", + "밥을 준 지 " + hoursSinceLastFeeding + "시간이 지났어요.", + "FEEDING" + ); + } + } + + private void sendWateringNotificationToAllGroupMembers(final List groupMembers, final PetProfile petProfile, final Long hoursSinceLastWatering) { + for (final PetGroupMember groupMember : groupMembers) { + final Long groupMemberId = groupMember.getMember().getId(); + + notificationService.sendNotification( + groupMemberId, + petProfile.getName() + "이에게 물을 교체해 줄 시간이에요!", + "물을 교체해 준 지 " + hoursSinceLastWatering + "시간이 지났어요.", + "WATERING" + ); + } + } +} diff --git a/src/main/java/com/petlog/petgroup/service/PetGroupService.java b/src/main/java/com/petlog/petgroup/service/PetGroupService.java index c47e264..bb1afcd 100644 --- a/src/main/java/com/petlog/petgroup/service/PetGroupService.java +++ b/src/main/java/com/petlog/petgroup/service/PetGroupService.java @@ -61,6 +61,7 @@ public void createPetGroup(final Long memberId, final CreatePetGroupDto dto) { final FeedingDailyRecord feedingDailyRecord = new FeedingDailyRecord( petProfile, member, + dto.lastFeedingTime(), null ); feedingDailyRecordRepository.save(feedingDailyRecord); @@ -68,6 +69,7 @@ public void createPetGroup(final Long memberId, final CreatePetGroupDto dto) { final WateringDailyRecord wateringDailyRecord = new WateringDailyRecord( petProfile, member, + dto.lastWateringTime(), null ); wateringDailyRecordRepository.save(wateringDailyRecord); diff --git a/src/main/java/com/petlog/common/config/S3Config.java b/src/main/java/com/petlog/s3/config/S3Config.java similarity index 96% rename from src/main/java/com/petlog/common/config/S3Config.java rename to src/main/java/com/petlog/s3/config/S3Config.java index 8b7a89c..00524e2 100644 --- a/src/main/java/com/petlog/common/config/S3Config.java +++ b/src/main/java/com/petlog/s3/config/S3Config.java @@ -1,4 +1,4 @@ -package com.petlog.common.config; +package com.petlog.s3.config; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSStaticCredentialsProvider; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 373bf06..00f777d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -20,3 +20,7 @@ cloud: credentials: access-key: ${AWS_ACCESS_KEY:access_key} secret-key: ${AWS_SECRET_KEY:secret_key} + +firebase: + petlog-firebase-key: + path: "/firebase/petlog-firebase-key.json" diff --git a/src/test/java/com/petlog/common/config/jwt/JwtFactory.java b/src/test/java/com/petlog/common/config/jwt/JwtFactory.java index 788bdae..6a53fe8 100644 --- a/src/test/java/com/petlog/common/config/jwt/JwtFactory.java +++ b/src/test/java/com/petlog/common/config/jwt/JwtFactory.java @@ -1,5 +1,6 @@ package com.petlog.common.config.jwt; +import com.petlog.auth.jwt.JwtProperties; import io.jsonwebtoken.Header; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; diff --git a/src/test/java/com/petlog/common/config/jwt/TokenProviderTest.java b/src/test/java/com/petlog/common/config/jwt/TokenProviderTest.java index 71f5dc8..569a4d9 100644 --- a/src/test/java/com/petlog/common/config/jwt/TokenProviderTest.java +++ b/src/test/java/com/petlog/common/config/jwt/TokenProviderTest.java @@ -1,5 +1,7 @@ package com.petlog.common.config.jwt; +import com.petlog.auth.jwt.JwtProperties; +import com.petlog.auth.jwt.TokenProvider; import com.petlog.member.entity.Member; import com.petlog.member.repository.MemberRepository; import io.jsonwebtoken.Jwts;