Skip to content

Commit

Permalink
[YS-201] feat: 추천 실험 공고 메일 전송 로직 구현 (#77)
Browse files Browse the repository at this point in the history
* feat: implements daily matching algorithms

- 매일 아침 07:55 실행되는 벌크 크론 잡의 매칭 알고리즘 로직 구현
- Querydsl 이용하여 '오늘' 등록된 공고와 모든 참여자들의 매칭 조건 비교

* feat: add missing busniess logic for matching algorithms

- 기획 반영 (매칭 공고 기준 시각 변경, 참여자 당 최대 10개)

* refact: optimize by filtering out null values before processing

* feat: define application layers

* feat: add spring actuator library to track scheduling jobs well

* feat: setting scheduler jobs for successful cron jobs

* test: add test codes for EmailMatchUseCaseTest

- `EmailMatchUseCaseTest` 테스트 코드 추가
- 기존의 `EmailServiceTest` 삭제

* test: add test codes for GetMatchingExperimentPostsUseCase

* refact: update Input type String to LocalDateTime

* fix: make UrlGeneratorGateway to prevent from hard-coding URL, to meet the DIP

* refact: rename email related usecases

* refact: reflect code reviews

- `List<<ExperimentPost>?>` 타입을 `List<ExperimentPost>` 리팩터링
- 로그 및 반환 메시지에 기존 이모지 제거
- 테스트의 용이성을 위해, 테스트용 API 구현

* refact: reflect code review

- 앞서 못지운 로그, 반환 메시지 이모지 제거
- 테스트용 트리거 주석처리, API 제거 처리
- 쿼리 작성 시 불필요한 문자열 파싱 제거

* fix: fix typo for SendEmailMatchingUseCase

* refact: update the codes to meet the DIP

- 기존의 postId별 생성된 공고들의 url generate로직을 DIP 원칙에 맞게
  개선

* fix: delete emoji to meet the requirements

* fix: delete unused comments for scheduler trigger
  • Loading branch information
chock-cho authored Feb 4, 2025
1 parent 9b5c233 commit 1bcfe61
Show file tree
Hide file tree
Showing 21 changed files with 625 additions and 93 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-mail")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("com.github.f4b6a3:tsid-creator:5.2.6")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
package com.dobby.backend.application.service

import com.dobby.backend.application.usecase.member.email.EmailCodeSendUseCase
import com.dobby.backend.application.usecase.member.email.EmailVerificationUseCase
import com.dobby.backend.application.usecase.member.email.SendEmailCodeUseCase
import com.dobby.backend.application.usecase.member.email.SendMatchingEmailUseCase
import com.dobby.backend.application.usecase.member.email.VerifyEmailUseCase
import com.dobby.backend.application.usecase.member.email.GetMatchingExperimentPostsUseCase
import jakarta.transaction.Transactional
import org.springframework.stereotype.Service

@Service
class EmailService(
private val emailCodeSendUseCase: EmailCodeSendUseCase,
private val emailVerificationUseCase: EmailVerificationUseCase
private val sendEmailCodeUseCase: SendEmailCodeUseCase,
private val verifyEmailUseCase: VerifyEmailUseCase,
private val sendMatchingEmailUseCase: SendMatchingEmailUseCase,
private val getMatchingExperimentPostsUseCase: GetMatchingExperimentPostsUseCase
) {
@Transactional
fun sendEmail(req: EmailCodeSendUseCase.Input) : EmailCodeSendUseCase.Output{
return emailCodeSendUseCase.execute(req)
fun sendEmail(req: SendEmailCodeUseCase.Input) : SendEmailCodeUseCase.Output{
return sendEmailCodeUseCase.execute(req)
}

@Transactional
fun verifyCode(req: EmailVerificationUseCase.Input) : EmailVerificationUseCase.Output {
return emailVerificationUseCase.execute(req)
fun verifyCode(req: VerifyEmailUseCase.Input) : VerifyEmailUseCase.Output {
return verifyEmailUseCase.execute(req)
}

@Transactional
fun sendMatchingEmail(req: SendMatchingEmailUseCase.Input): SendMatchingEmailUseCase.Output{
return sendMatchingEmailUseCase.execute(req)
}

@Transactional
fun getMatchingInfo(req: GetMatchingExperimentPostsUseCase.Input): GetMatchingExperimentPostsUseCase.Output{
return getMatchingExperimentPostsUseCase.execute(req)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.dobby.backend.application.usecase.member.email

import com.dobby.backend.application.usecase.UseCase
import com.dobby.backend.domain.gateway.experiment.ExperimentPostGateway
import com.dobby.backend.domain.model.experiment.ExperimentPost
import java.time.LocalDateTime

class GetMatchingExperimentPostsUseCase(
private val experimentPostGateway: ExperimentPostGateway
): UseCase<GetMatchingExperimentPostsUseCase.Input, GetMatchingExperimentPostsUseCase.Output> {

data class Input(
val requestTime: LocalDateTime
)

data class Output(
val matchingPosts: Map<String, List<ExperimentPost>>
)

override fun execute(input : Input): Output {
val result = experimentPostGateway.findMatchingExperimentPosts()
return Output(matchingPosts = result)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import com.dobby.backend.domain.model.Verification
import com.dobby.backend.infrastructure.database.entity.enums.VerificationStatus
import com.dobby.backend.util.EmailUtils

class EmailCodeSendUseCase(
class SendEmailCodeUseCase(
private val verificationGateway: VerificationGateway,
private val emailGateway: EmailGateway,
private val idGeneratorGateway: IdGeneratorGateway
) : UseCase<EmailCodeSendUseCase.Input, EmailCodeSendUseCase.Output> {
) : UseCase<SendEmailCodeUseCase.Input, SendEmailCodeUseCase.Output> {

data class Input(
val univEmail: String
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.dobby.backend.application.usecase.member.email

import com.dobby.backend.application.usecase.UseCase
import com.dobby.backend.domain.exception.EmailDomainNotFoundException
import com.dobby.backend.domain.gateway.UrlGeneratorGateway
import com.dobby.backend.domain.gateway.email.EmailGateway
import com.dobby.backend.domain.model.experiment.ExperimentPost
import com.dobby.backend.util.EmailUtils
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

class SendMatchingEmailUseCase(
private val emailGateway: EmailGateway,
private val urlGeneratorGateway: UrlGeneratorGateway
): UseCase<SendMatchingEmailUseCase.Input, SendMatchingEmailUseCase.Output>{

data class Input(
val contactEmail: String,
val experimentPosts: List<ExperimentPost>,
val currentDateTime: LocalDateTime
)

data class Output(
val isSuccess: Boolean,
val message: String
)

override fun execute(input: Input): Output {
validateEmail(input.contactEmail)

val (title, content) = getFormattedEmail(input.contactEmail, input.experimentPosts)

return try {
emailGateway.sendEmail(input.contactEmail, title, content)
Output(isSuccess = true, message = " Email successfully sent to ${input.contactEmail}")
} catch (ex: Exception) {
Output(isSuccess = false, message = "Failed to send to email to ${input.contactEmail}: ${ex.message}")
}
}

private fun validateEmail(email : String){
if(!EmailUtils.isDomainExists(email)) throw EmailDomainNotFoundException
}

private fun getFormattedEmail(memberName: String, experimentPosts: List<ExperimentPost>): Pair<String, String> {
val todayDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
val emailTitle = "[그라밋🔬] $todayDate 오늘의 추천 실험 공고를 확인해보세요!"
val jobListFormatted = experimentPosts.joinToString("\n\n") { post ->
val postUrl = urlGeneratorGateway.getExperimentPostUrl(postId = post.id)
"""
🔹 **${post.title}**
- 기간: ${post.startDate} ~ ${post.endDate}
- 위치: ${post.univName ?: "공고참고"}
- 보상: ${post.reward}
- [공고 확인하기]($postUrl)
""".trimIndent()
}

val content = """
${memberName}님과 꼭 맞는 실험 공고를 찾아왔어요 🧚
* 자세한 실험 내용이나 모집 대상은 공고 내용을 확인해 주세요.
🔹 **추천 공고 목록** 🔹
$jobListFormatted
더 많은 공고를 보려면 [그라밋 웹사이트](${urlGeneratorGateway.getBaseUrl()})를 방문해 주세요!
""".trimIndent()

return Pair(emailTitle, content)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import com.dobby.backend.domain.exception.CodeNotCorrectException
import com.dobby.backend.domain.exception.VerifyInfoNotFoundException
import com.dobby.backend.domain.gateway.email.VerificationGateway

class EmailVerificationUseCase(
class VerifyEmailUseCase(
private val verificationGateway: VerificationGateway
) : UseCase<EmailVerificationUseCase.Input, EmailVerificationUseCase.Output> {
) : UseCase<VerifyEmailUseCase.Input, VerifyEmailUseCase.Output> {

data class Input (
val univEmail : String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.dobby.backend.domain.gateway

interface UrlGeneratorGateway {
fun getBaseUrl(): String
fun getExperimentPostUrl(postId: String): String
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.dobby.backend.domain.model.experiment.ExperimentPost
import com.dobby.backend.infrastructure.database.entity.enums.areaInfo.Region
import jakarta.persistence.Tuple
import java.time.LocalDate
import java.time.LocalDateTime

interface ExperimentPostGateway {
fun save(experimentPost: ExperimentPost): ExperimentPost
Expand All @@ -26,4 +27,5 @@ interface ExperimentPostGateway {
fun findExperimentPostByMemberIdAndPostId(memberId: String, postId: String): ExperimentPost?
fun countExperimentPostsByCustomFilter(customFilter: CustomFilter): Int
fun delete(post: ExperimentPost)
fun findMatchingExperimentPosts(): Map<String, List<ExperimentPost>>
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,59 @@
package com.dobby.backend.infrastructure.config

import com.dobby.backend.infrastructure.scheduler.ExpiredExperimentPostJob
import com.dobby.backend.infrastructure.scheduler.SendMatchingEmailJob
import org.quartz.*
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.util.*

@Configuration
class SchedulerConfig {
private val logger: Logger = LoggerFactory.getLogger(SchedulerConfig::class.java)

@Bean
fun expiredExperimentPostJobDetail(): JobDetail {
logger.info("Registering ExpiredExperimentPostJob...")
return JobBuilder.newJob(ExpiredExperimentPostJob::class.java)
.withIdentity("expired_experiment_post_job")
.withIdentity("expired_experiment_post_job", "DEFAULT")
.storeDurably()
.build()
}

@Bean
fun expiredExperimentPostTrigger(): Trigger {
logger.info("Registering ExpiredExperimentPostTrigger...")
return TriggerBuilder.newTrigger()
.forJob(expiredExperimentPostJobDetail())
.withIdentity("expired_experiment_post_trigger")
.withIdentity("expired_experiment_post_trigger", "DEFAULT")
.withSchedule(
CronScheduleBuilder.dailyAtHourAndMinute(0, 0)
.inTimeZone(TimeZone.getTimeZone("Asia/Seoul"))
)
.build()
}

@Bean
fun sendEmailMatchingJobDetail(): JobDetail {
logger.info("Registering SendEmailMatchingJobDetail...")
return JobBuilder.newJob(SendMatchingEmailJob::class.java)
.withIdentity("matching_email_send_job", "DEFAULT")
.storeDurably()
.build()
}

@Bean
fun sendEmailMatchingPostTrigger() : Trigger {
logger.info("Registering SendEmailMatchingPostTrigger...")
return TriggerBuilder.newTrigger()
.forJob(sendEmailMatchingJobDetail())
.withIdentity("send_matching_email_trigger", "DEFAULT")
.withSchedule(
CronScheduleBuilder.dailyAtHourAndMinute(8,0)
.inTimeZone(TimeZone.getTimeZone("Asia/Seoul"))
)
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.dobby.backend.infrastructure.config.properties

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component

@Component
@ConfigurationProperties(prefix = "swagger")
class UrlProperties (
var serverUrl : String = ""
)

Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ interface ExperimentPostCustomRepository {
): List<ExperimentPostEntity>?

fun countExperimentPostsByCustomFilter(customFilter: CustomFilter): Int

fun findMatchingExperimentPostsForAllParticipants(): Map<String, List<ExperimentPostEntity>>
}
Loading

0 comments on commit 1bcfe61

Please sign in to comment.