Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

Commit

Permalink
[Feat] fcm alaram #26 (#29)
Browse files Browse the repository at this point in the history
* setting: 서브모듈 갱신과 jvmTarget 17로 명시

* feat(Events): Events 구현

* feat(ReminderService): 물주기 알림을 주는 ReminderService 구현

* feat(FirebaseConfig): FCM scope 추가

* refactor(FCMChannel): 페키지 이동
  • Loading branch information
zbqmgldjfh authored Jan 22, 2024
1 parent d2f9622 commit acb9b2f
Show file tree
Hide file tree
Showing 14 changed files with 255 additions and 2 deletions.
8 changes: 8 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,11 @@ tasks.withType<KotlinCompile> {
tasks.withType<Test> {
useJUnitPlatform()
}
val compileKotlin: KotlinCompile by tasks
compileKotlin.kotlinOptions {
jvmTarget = "17"
}
val compileTestKotlin: KotlinCompile by tasks
compileTestKotlin.kotlinOptions {
jvmTarget = "17"
}
19 changes: 19 additions & 0 deletions src/main/kotlin/gdsc/plantory/common/config/EventsConfiguration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package gdsc.plantory.common.config

import gdsc.plantory.event.Events
import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class EventsConfiguration(
@Autowired val applicationContext: ApplicationContext,
) {

@Bean
fun eventsInitializer(): InitializingBean {
return InitializingBean { Events.setPublisher(applicationContext) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import java.util.Optional
@Configuration
class FirebaseConfig(
@Value("\${fcm.key.path}")
private val fcmPrivateKeyPath: String
private val fcmPrivateKeyPath: String,
@Value("\${fcm.key.scope}")
private val fcmScope: String
) {

@Bean
Expand Down Expand Up @@ -48,5 +50,6 @@ class FirebaseConfig(
private fun createGoogleCredentials(): GoogleCredentials {
return GoogleCredentials
.fromStream(ClassPathResource(fcmPrivateKeyPath).inputStream)
.createScoped(fcmScope)
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/gdsc/plantory/event/Events.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package gdsc.plantory.event

import org.springframework.context.ApplicationEventPublisher
import java.util.Objects

class Events {
companion object {
private lateinit var publisher: ApplicationEventPublisher

fun setPublisher(publisher: ApplicationEventPublisher) {
Events.publisher = publisher
}

fun raise(event: Any) {
if (Objects.nonNull(publisher)) {
publisher.publishEvent(event)
}
}
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/gdsc/plantory/event/FCMChannel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package gdsc.plantory.event

enum class FCMChannel {
WATER_ALERT,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package gdsc.plantory.event.notification

data class WaterCycleEvent(
val deviceToken: String,
val title: String = "물을 줄 시간이에요!",
val body: String = "반려 식물에게 물을 줄 시간이에요!",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package gdsc.plantory.event.notification

import com.google.firebase.messaging.AndroidConfig
import com.google.firebase.messaging.AndroidNotification
import com.google.firebase.messaging.FirebaseMessaging
import com.google.firebase.messaging.FirebaseMessagingException
import com.google.firebase.messaging.Message
import com.google.firebase.messaging.Notification
import gdsc.plantory.event.FCMChannel
import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Component
import org.springframework.transaction.event.TransactionPhase
import org.springframework.transaction.event.TransactionalEventListener

@Component
class WaterCycleEventListener(
val firebaseMessaging: FirebaseMessaging,
) {

companion object {
private val log = LoggerFactory.getLogger(WaterCycleEventListener::class.java)
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async
fun sendFcmNotification(events: List<WaterCycleEvent>) {
val messages: List<Message> = createMessages(events, FCMChannel.WATER_ALERT.name)

try {
firebaseMessaging.sendEach(messages)
} catch (e: FirebaseMessagingException) {
log.warn("fail send FCM message", e)
}
}

private fun createMessages(events: List<WaterCycleEvent>, channelId: String): List<Message> {
return events.map { event -> createMessage(event, channelId) }.toList()
}

private fun createMessage(event: WaterCycleEvent, channelId: String): Message {
return Message.builder()
.setNotification(
Notification
.builder()
.setTitle(event.title)
.setBody(event.body)
.build()
)
.setAndroidConfig(createAndroidConfig(channelId))
.setToken(event.deviceToken)
.build()
}

private fun createAndroidConfig(channelId: String): AndroidConfig? {
return AndroidConfig.builder()
.setNotification(createAndroidNotification(channelId))
.build()
}

private fun createAndroidNotification(channelId: String): AndroidNotification? {
return AndroidNotification.builder()
.setChannelId(channelId)
.build()
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gdsc.plantory.plant.domain

import NotFoundException
import gdsc.plantory.plant.presentation.dto.CompanionPlantWaterCycleDto
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import java.time.LocalDate
Expand All @@ -19,6 +20,19 @@ interface CompanionPlantRepository : JpaRepository<CompanionPlant, Long> {
fun findByIdAndMemberId(id: Long, memberId: Long): CompanionPlant?
fun removeByIdAndMemberId(id: Long, memberId: Long)

@Query(
"""
SELECT new gdsc.plantory.plant.presentation.dto.CompanionPlantWaterCycleDto(
member.deviceToken,
plant.nickname._value
)
FROM Member member JOIN CompanionPlant plant
ON member.id = plant.memberId
WHERE plant.nextWaterDate = :date
"""
)
fun findAllByNextWaterDate(date: LocalDate): List<CompanionPlantWaterCycleDto>

@Query(
"""
SELECT history FROM PlantHistory history
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package gdsc.plantory.plant.presentation.dto

class CompanionPlantWaterCycleDto(
val deviceToken: String,
val nickName: String,
)
35 changes: 35 additions & 0 deletions src/main/kotlin/gdsc/plantory/plant/service/ReminderService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package gdsc.plantory.plant.service

import gdsc.plantory.event.Events
import gdsc.plantory.event.notification.WaterCycleEvent
import gdsc.plantory.plant.domain.CompanionPlantRepository
import gdsc.plantory.plant.presentation.dto.CompanionPlantWaterCycleDto
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDate

@Service
@Transactional
class ReminderService(
private val companionPlantRepository: CompanionPlantRepository,
) {

@Scheduled(cron = "0 0 8 * * *")
fun sendWaterNotification() {
val companionPlants = companionPlantRepository.findAllByNextWaterDate(LocalDate.now())
val events: List<WaterCycleEvent> = buildWaterCycleEvents(companionPlants)
Events.raise(events)
}

private fun buildWaterCycleEvents(companionPlants: List<CompanionPlantWaterCycleDto>) =
companionPlants
.map {
WaterCycleEvent(
it.deviceToken,
"물을 줄 시간이에요!",
"${it.nickName}에게 물을 줄 시간이에요!"
)
}
.toList()
}
1 change: 1 addition & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ companionPlant:
fcm:
key:
path: src/main/resources/config/google-services.json
scope: prod
2 changes: 1 addition & 1 deletion src/main/resources/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package gdsc.plantory.plant.domain

import gdsc.plantory.member.domain.Member
import gdsc.plantory.member.domain.MemberRepository
import gdsc.plantory.util.AcceptanceTest
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.groups.Tuple
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import java.time.LocalDate

@DisplayName("리포지토리 : CompanionPlant")
class CompanionPlantRepositoryTest(
@Autowired val companionPlantRepository: CompanionPlantRepository,
@Autowired val memberRepository: MemberRepository,
) : AcceptanceTest() {

@Test
fun `물주는 날짜가 된 반려식물의 별칭과 해당 유저의 deviceToken을 조회한다`() {
// given
val member = Member("shine")
val savedMember = memberRepository.save(member)
val memberId = savedMember.getId

val nextWaterDate = LocalDate.of(2024, 1, 7)

val companionPlant1 = createCompanionPlantByLastWaterDate(nextWaterDate, memberId)
val companionPlant2 = createCompanionPlantByLastWaterDate(nextWaterDate.plusDays(1), memberId)
val companionPlant3 = createCompanionPlantByLastWaterDate(nextWaterDate.minusDays(1), memberId)
val companionPlant4 = createCompanionPlantByLastWaterDate(nextWaterDate, memberId)
companionPlantRepository.saveAll(
listOf(
companionPlant1,
companionPlant2,
companionPlant3,
companionPlant4
)
)

// when
val results = companionPlantRepository.findAllByNextWaterDate(nextWaterDate)

// then
assertThat(results).hasSize(2)
.extracting("deviceToken", "nickName")
.containsExactlyInAnyOrder(
Tuple.tuple("shine", "2024-01-07"),
Tuple.tuple("shine", "2024-01-07")
)
}

private fun createCompanionPlantByLastWaterDate(nextWaterDate: LocalDate, memberId: Long) = CompanionPlant(
_imageUrl = "https://nongsaro.go.kr/cms_contents/301/13336_MF_ATTACH_05.jpg",
_shortDescription = "덕구리난은 덕구리난과!",
_nickname = nextWaterDate.toString(),
birthDate = LocalDate.of(2024, 1, 1),
nextWaterDate = nextWaterDate,
lastWaterDate = LocalDate.of(2024, 1, 4),
waterCycle = 3,
plantInformationId = 1L,
memberId = memberId,
)
}
5 changes: 5 additions & 0 deletions src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ local:
companionPlant:
image:
directory: photos/companionPlant

fcm:
key:
path: config/google-services.json
scope: test

0 comments on commit acb9b2f

Please sign in to comment.