Skip to content

Commit

Permalink
Merge pull request #106 from dnd-side-project/feat/alram_schedular
Browse files Browse the repository at this point in the history
[BLOOM-072] 물 주기 알림 스케줄러 작성
  • Loading branch information
stophwan authored Sep 12, 2024
2 parents 6ed049c + 5620e8c commit 90c1f6f
Show file tree
Hide file tree
Showing 20 changed files with 429 additions and 26 deletions.
5 changes: 5 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,14 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("io.github.oshai:kotlin-logging-jvm:5.1.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
runtimeOnly("com.mysql:mysql-connector-j")
testImplementation("com.h2database:h2")

// Batch
implementation("org.springframework.boot:spring-boot-starter-batch")
testImplementation("org.springframework.batch:spring-batch-test")

// Validation
implementation("org.springframework.boot:spring-boot-starter-validation")

Expand Down
19 changes: 19 additions & 0 deletions src/main/kotlin/dnd11th/blooming/batch/BatchConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dnd11th.blooming.batch

import org.springframework.beans.factory.support.BeanDefinitionRegistry
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.annotation.EnableScheduling

@Configuration
@EnableScheduling
class BatchConfig {
@Bean
fun jobRegistryBeanPostProcessorRemover(): BeanDefinitionRegistryPostProcessor? {
return BeanDefinitionRegistryPostProcessor {
registry: BeanDefinitionRegistry ->
registry.removeBeanDefinition("jobRegistryBeanPostProcessor")
}
}
}
28 changes: 28 additions & 0 deletions src/main/kotlin/dnd11th/blooming/batch/NotificationScheduler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dnd11th.blooming.batch

import dnd11th.blooming.common.util.Logger.Companion.log
import org.springframework.batch.core.Job
import org.springframework.batch.core.JobParameters
import org.springframework.batch.core.JobParametersBuilder
import org.springframework.batch.core.launch.JobLauncher
import org.springframework.stereotype.Component
import org.springframework.util.StopWatch
import java.util.concurrent.TimeUnit

@Component
class NotificationScheduler(
private val jobLauncher: JobLauncher,
private val notificationJob: Job,
) {
fun run() {
val jobParameters: JobParameters =
JobParametersBuilder()
.addLong("time", System.currentTimeMillis())
.toJobParameters()
val stopWatch = StopWatch()
stopWatch.start()
jobLauncher.run(notificationJob, jobParameters)
stopWatch.stop()
log.info { stopWatch.getTotalTime(TimeUnit.MILLISECONDS) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package dnd11th.blooming.batch

import dnd11th.blooming.client.fcm.PushNotification
import org.springframework.batch.core.Job
import org.springframework.batch.core.Step
import org.springframework.batch.core.configuration.annotation.JobScope
import org.springframework.batch.core.job.builder.JobBuilder
import org.springframework.batch.core.launch.support.RunIdIncrementer
import org.springframework.batch.core.repository.JobRepository
import org.springframework.batch.core.step.builder.StepBuilder
import org.springframework.batch.item.ItemProcessor
import org.springframework.batch.item.ItemReader
import org.springframework.batch.item.ItemWriter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.transaction.PlatformTransactionManager

@Configuration
class PlantNotificationJobConfig {
companion object {
const val CHUNK_SIZE: Int = 100
}

@Bean
fun notificationJob(
jobRepository: JobRepository,
waterNotificationStep: Step,
): Job {
return JobBuilder("notificationJob", jobRepository)
.incrementer(RunIdIncrementer())
.start(waterNotificationStep)
.build()
}

@Bean
@JobScope
fun waterNotificationStep(
jobRepository: JobRepository,
transactionManager: PlatformTransactionManager,
waterNotificationItemReader: ItemReader<UserPlantDto>,
waterNotificationItemProcessor: ItemProcessor<UserPlantDto, PushNotification>,
waterNotificationItemWriter: ItemWriter<PushNotification>,
): Step {
return StepBuilder("waterNotificationStep", jobRepository)
.chunk<UserPlantDto, PushNotification>(CHUNK_SIZE, transactionManager)
.reader(waterNotificationItemReader)
.processor(waterNotificationItemProcessor)
.writer(waterNotificationItemWriter)
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dnd11th.blooming.batch

import dnd11th.blooming.client.fcm.PushNotification
import org.springframework.batch.core.configuration.annotation.StepScope
import org.springframework.batch.item.ItemProcessor
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class PlantNotificationProcessor {
@Bean
@StepScope
fun waterNotificationItemProcessor(): ItemProcessor<UserPlantDto, PushNotification> {
return ItemProcessor { userPlantDto ->
PushNotification.create(userPlantDto)
}
}
}
27 changes: 27 additions & 0 deletions src/main/kotlin/dnd11th/blooming/batch/PlantNotificationReader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package dnd11th.blooming.batch

import dnd11th.blooming.domain.entity.user.AlarmTime
import dnd11th.blooming.domain.repository.myplant.MyPlantRepository
import org.springframework.batch.core.configuration.annotation.StepScope
import org.springframework.batch.item.support.ListItemReader
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.LocalTime

@Configuration
class PlantNotificationReader(
private val myPlantRepository: MyPlantRepository,
) {
@Bean
@StepScope
fun waterNotificationItemReader(): ListItemReader<UserPlantDto> {
val now: LocalTime = LocalTime.now()
val alarmTime = AlarmTime.fromHour(now)

val userPlantByAlarmTime: List<UserPlantDto> =
myPlantRepository.findPlantsByAlarmTimeInBatch(
alarmTime,
)
return ListItemReader(userPlantByAlarmTime)
}
}
29 changes: 29 additions & 0 deletions src/main/kotlin/dnd11th/blooming/batch/PlantNotificationWriter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package dnd11th.blooming.batch

import dnd11th.blooming.client.fcm.FcmService
import dnd11th.blooming.client.fcm.PushNotification
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.springframework.batch.core.configuration.annotation.StepScope
import org.springframework.batch.item.ItemWriter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class PlantNotificationWriter(
private val fcmService: FcmService,
) {
@Bean
@StepScope
fun waterNotificationItemWriter(): ItemWriter<PushNotification> {
return ItemWriter { pushNotifications ->
runBlocking {
pushNotifications.forEach { pushNotification ->
launch {
fcmService.send(pushNotification) // 비동기 처리
}
}
}
}
}
}
12 changes: 12 additions & 0 deletions src/main/kotlin/dnd11th/blooming/batch/UserPlantDto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dnd11th.blooming.batch

import java.time.LocalDate

data class UserPlantDto(
val userId: Long,
val userEmail: String,
val myPlantId: Long,
val plantNickname: String,
val lastWateredDate: LocalDate,
val waterPeriod: Int,
)
4 changes: 3 additions & 1 deletion src/main/kotlin/dnd11th/blooming/client/fcm/FcmService.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dnd11th.blooming.client.fcm

interface FcmService {
fun sendNotification(pushNotification: PushNotification)
suspend fun send(pushNotification: PushNotification)

suspend fun mock(pushNotification: PushNotification)
}
12 changes: 11 additions & 1 deletion src/main/kotlin/dnd11th/blooming/client/fcm/FcmServiceImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import com.google.firebase.FirebaseApp
import com.google.firebase.messaging.FirebaseMessaging
import com.google.firebase.messaging.Message
import com.google.firebase.messaging.Notification
import kotlinx.coroutines.delay
import org.springframework.stereotype.Service

@Service
class FcmServiceImpl : FcmService {
override fun sendNotification(pushNotification: PushNotification) {
override suspend fun send(pushNotification: PushNotification) {
val notification: Notification =
Notification.builder()
.setTitle(pushNotification.title)
Expand All @@ -27,4 +28,13 @@ class FcmServiceImpl : FcmService {
FirebaseMessaging.getInstance().send(message)
}
}

override suspend fun mock(pushNotification: PushNotification) {
try {
// 100ms 동안 스레드를 대기
delay(100)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
}
15 changes: 14 additions & 1 deletion src/main/kotlin/dnd11th/blooming/client/fcm/PushNotification.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
package dnd11th.blooming.client.fcm

import dnd11th.blooming.batch.UserPlantDto

data class PushNotification(
val myPlantId: Long,
val token: String,
val title: String,
val content: String,
)
) {
companion object {
fun create(userPlantDto: UserPlantDto): PushNotification {
return PushNotification(
myPlantId = userPlantDto.myPlantId,
token = "deviceToken",
title = "블루밍",
content = "${userPlantDto.plantNickname}에 물이 필요해요",
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ enum class GrowTemperature(
val lowTemperature: Int,
val highTemperature: Int,
) {
GROW_TEMPERATURE_10_25(
GROW_TEMPERATURE_10_15(
10,
15,
),
Expand Down
47 changes: 27 additions & 20 deletions src/main/kotlin/dnd11th/blooming/domain/entity/user/AlarmTime.kt
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
package dnd11th.blooming.domain.entity.user

enum class AlarmTime(val code: Int, val displayName: String) {
TIME_5_6(1, "5:00-6:00"),
TIME_6_7(2, "6:00-7:00"),
TIME_7_8(3, "7:00-8:00"),
TIME_8_9(4, "8:00-9:00"),
TIME_9_10(5, "9:00-10:00"),
TIME_10_11(6, "10:00-11:00"),
TIME_11_12(7, "11:00-12:00"),
TIME_12_13(8, "12:00-13:00"),
TIME_13_14(9, "13:00-14:00"),
TIME_14_15(10, "14:00-15:00"),
TIME_15_16(11, "15:00-16:00"),
TIME_16_17(12, "16:00-17:00"),
TIME_17_18(13, "17:00-18:00"),
TIME_18_19(14, "18:00-19:00"),
TIME_19_20(15, "19:00-20:00"),
TIME_20_21(16, "20:00-21:00"),
TIME_21_22(17, "21:00-22:00"),
TIME_22_23(18, "22:00-23:00"),
TIME_23_24(19, "23:00-24:00"),
import java.time.LocalTime

enum class AlarmTime(val code: Int, val displayName: String, val startHour: Int, val endHour: Int) {
TIME_5_6(1, "5:00-6:00", 5, 6),
TIME_6_7(2, "6:00-7:00", 6, 7),
TIME_7_8(3, "7:00-8:00", 7, 8),
TIME_8_9(4, "8:00-9:00", 8, 9),
TIME_9_10(5, "9:00-10:00", 9, 10),
TIME_10_11(6, "10:00-11:00", 10, 11),
TIME_11_12(7, "11:00-12:00", 11, 12),
TIME_12_13(8, "12:00-13:00", 12, 13),
TIME_13_14(9, "13:00-14:00", 13, 14),
TIME_14_15(10, "14:00-15:00", 14, 15),
TIME_15_16(11, "15:00-16:00", 15, 16),
TIME_16_17(12, "16:00-17:00", 16, 17),
TIME_17_18(13, "17:00-18:00", 17, 18),
TIME_18_19(14, "18:00-19:00", 18, 19),
TIME_19_20(15, "19:00-20:00", 19, 20),
TIME_20_21(16, "20:00-21:00", 20, 21),
TIME_21_22(17, "21:00-22:00", 21, 22),
TIME_22_23(18, "22:00-23:00", 22, 23),
TIME_23_24(19, "23:00-24:00", 23, 24),
;

companion object {
fun from(code: Int): AlarmTime {
return entries.find { it.code == code }
?: throw IllegalArgumentException("Invalid AlarmTime code: $code")
}

fun fromHour(now: LocalTime): AlarmTime {
return entries.find { now.hour in it.startHour until it.endHour }
?: throw IllegalArgumentException("No matching AlarmTime for hour: ${now.hour}")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package dnd11th.blooming.domain.repository.myplant

import dnd11th.blooming.batch.UserPlantDto
import dnd11th.blooming.domain.entity.user.AlarmTime

interface MyPlantQueryDslRepository {
fun findPlantsByAlarmTimeInBatch(alarmTime: AlarmTime): List<UserPlantDto>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package dnd11th.blooming.domain.repository.myplant

import com.querydsl.core.types.Projections
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.jpa.impl.JPAQueryFactory
import dnd11th.blooming.batch.UserPlantDto
import dnd11th.blooming.domain.entity.QMyPlant
import dnd11th.blooming.domain.entity.user.AlarmTime
import dnd11th.blooming.domain.entity.user.QUser
import org.springframework.stereotype.Repository

@Repository
class MyPlantQueryDslRepositoryImpl(
private val queryFactory: JPAQueryFactory,
) : MyPlantQueryDslRepository {
override fun findPlantsByAlarmTimeInBatch(alarmTime: AlarmTime): List<UserPlantDto> {
val myPlant = QMyPlant.myPlant
val user = QUser.user
return queryFactory
.select(
Projections.constructor(
UserPlantDto::class.java,
user.id,
user.email,
myPlant.id,
myPlant.nickname,
myPlant.lastWateredDate,
myPlant.alarm.waterPeriod,
),
)
.from(myPlant)
.join(myPlant.user, user)
.where(
user.alarmStatus.isTrue,
user.alarmTime.eq(alarmTime),
myPlant.alarm.waterAlarm.isTrue,
Expressions.numberTemplate(
Int::class.java,
"DATEDIFF(CURRENT_DATE, {0})",
myPlant.lastWateredDate,
)
.eq(myPlant.alarm.waterPeriod),
)
.fetch()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param

interface MyPlantRepository : JpaRepository<MyPlant, Long> {
interface MyPlantRepository : JpaRepository<MyPlant, Long>, MyPlantQueryDslRepository {
fun findByIdAndUser(
id: Long,
user: User,
Expand Down
Loading

0 comments on commit 90c1f6f

Please sign in to comment.