Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.didit.adapter.integration.scheduler

import com.didit.application.achievement.provided.AchievementDeletionPort
import com.didit.application.auth.required.RefreshTokenRepository
import com.didit.application.auth.required.UserRepository
import com.didit.application.notification.provided.NotificationDeletionPort
import com.didit.application.organization.provided.OrganizationDeletionPort
import com.didit.application.retrospect.provided.RetrospectDeletionPort
import com.didit.application.retrospect.required.RetrospectiveRepository
import jakarta.transaction.Transactional
import org.springframework.stereotype.Component
import java.time.LocalDateTime
import java.util.UUID

@Component
class CleanupExecutor(
private val retrospectiveRepository: RetrospectiveRepository,
private val refreshTokenRepository: RefreshTokenRepository,
private val userRepository: UserRepository,
private val projectDeletionPort: OrganizationDeletionPort,
private val notificationDeletionPort: NotificationDeletionPort,
private val retrospectDeletionPort: RetrospectDeletionPort,
private val achievementDeletionPort: AchievementDeletionPort,
) {
@Transactional
fun cleanPendingRetrospects(): Int {
val cutoff = LocalDateTime.now().minusDays(1)
val targets = retrospectiveRepository.findAllPendingBefore(cutoff)
targets.forEach { retrospectiveRepository.delete(it) }
return targets.size
}

@Transactional
fun cleanExpiredRefreshTokens(): Int = refreshTokenRepository.deleteAllExpiredBefore(LocalDateTime.now())

fun findWithdrawnToAnonymize(): List<UUID> =
userRepository.findAllWithdrawnAndNotAnonymizedBefore(LocalDateTime.now().minusDays(30)).map { it.id }

fun findWithdrawnToDelete(): List<UUID> =
userRepository.findAllWithdrawnAndAnonymizedBefore(LocalDateTime.now().minusDays(90)).map { it.id }

@Transactional
fun anonymize(userId: UUID) {
val user = userRepository.findById(userId) ?: return
user.anonymize()
userRepository.save(user)
}

@Transactional
fun deleteUser(userId: UUID) {
val user = userRepository.findById(userId) ?: return
projectDeletionPort.deleteByUserId(userId)
notificationDeletionPort.deleteByUserId(userId)
retrospectDeletionPort.deleteByUserId(userId)
achievementDeletionPort.deleteByUserId(userId)
userRepository.delete(user)
}
}
Original file line number Diff line number Diff line change
@@ -1,52 +1,34 @@
package com.didit.adapter.integration.scheduler

import com.didit.application.auth.required.RefreshTokenRepository
import com.didit.application.auth.required.UserRepository
import com.didit.application.notification.provided.NotificationDeletionPort
import com.didit.application.organization.provided.OrganizationDeletionPort
import com.didit.application.retrospect.provided.RetrospectDeletionPort
import com.didit.application.retrospect.required.RetrospectiveRepository
import jakarta.transaction.Transactional
import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import java.time.LocalDateTime

@Component
class CleanupScheduler(
private val retrospectiveRepository: RetrospectiveRepository,
private val refreshTokenRepository: RefreshTokenRepository,
private val userRepository: UserRepository,
private val projectDeletionPort: OrganizationDeletionPort,
private val notificationDeletionPort: NotificationDeletionPort,
private val retrospectDeletionPort: RetrospectDeletionPort,
private val cleanupExecutor: CleanupExecutor,
) {
companion object {
private val logger = LoggerFactory.getLogger(CleanupScheduler::class.java)
}

@Scheduled(cron = "0 0 5 * * *")
@Transactional
fun cleanup() {
cleanPendingRetrospects()
cleanExpiredRefreshTokens()
cleanWithdrawnUsers()
}

private fun cleanPendingRetrospects() {
val cutoff = LocalDateTime.now().minusDays(1)

val targets = retrospectiveRepository.findAllPendingBefore(cutoff)

targets.forEach { retrospectiveRepository.delete(it) }

logger.info("PENDING 회고 삭제 - count: ${targets.size}")
runCatching { cleanupExecutor.cleanPendingRetrospects() }
.onSuccess { logger.info("PENDING 회고 삭제 - count: $it") }
.onFailure { logger.error("PENDING 회고 삭제 실패", it) }
}

private fun cleanExpiredRefreshTokens() {
val count = refreshTokenRepository.deleteAllExpiredBefore(LocalDateTime.now())

logger.info("만료 리프레시 토큰 삭제 - count: $count")
runCatching { cleanupExecutor.cleanExpiredRefreshTokens() }
.onSuccess { logger.info("만료 리프레시 토큰 삭제 - count: $it") }
.onFailure { logger.error("만료 리프레시 토큰 삭제 실패", it) }
}

private fun cleanWithdrawnUsers() {
Expand All @@ -55,27 +37,22 @@ class CleanupScheduler(
}

private fun anonymizeWithdrawnUsers() {
val cutoff = LocalDateTime.now().minusDays(30)
val targets = userRepository.findAllWithdrawnAndNotAnonymizedBefore(cutoff)

targets.forEach { user ->
user.anonymize()
userRepository.save(user)
var count = 0
cleanupExecutor.findWithdrawnToAnonymize().forEach { userId ->
runCatching { cleanupExecutor.anonymize(userId) }
.onSuccess { count++ }
.onFailure { logger.error("탈퇴 유저 익명화 실패 - userId: $userId", it) }
}

logger.info("탈퇴 유저 익명화 - count: ${targets.size}")
logger.info("탈퇴 유저 익명화 - count: $count")
}

private fun deleteWithdrawnUsers() {
val cutoff = LocalDateTime.now().minusDays(90)
val targets = userRepository.findAllWithdrawnAndAnonymizedBefore(cutoff)

targets.forEach { user ->
projectDeletionPort.deleteByUserId(user.id)
notificationDeletionPort.deleteByUserId(user.id)
retrospectDeletionPort.deleteByUserId(user.id)
userRepository.delete(user)
var count = 0
cleanupExecutor.findWithdrawnToDelete().forEach { userId ->
runCatching { cleanupExecutor.deleteUser(userId) }
.onSuccess { count++ }
.onFailure { logger.error("탈퇴 유저 삭제 실패 - userId: $userId", it) }
}
logger.info("탈퇴 유저 완전 삭제 - count: ${targets.size}")
logger.info("탈퇴 유저 완전 삭제 - count: $count")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.didit.adapter.integration.scheduler

import com.didit.application.achievement.required.UserMissionRepository
import com.didit.application.retrospect.required.RetrospectiveRepository
import com.didit.domain.achievement.UserMission
import com.didit.domain.shared.ServiceTime
import jakarta.transaction.Transactional
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import java.time.DayOfWeek
import java.time.temporal.TemporalAdjusters

@Component
class MissionFailureProcessor(
private val userMissionRepository: UserMissionRepository,
private val retrospectiveRepository: RetrospectiveRepository,
) {
companion object {
private val logger = LoggerFactory.getLogger(MissionFailureProcessor::class.java)
}

@Transactional
fun failExpiredLv2(userMission: UserMission) {
userMission.setFailureWaitingConfirm()
userMissionRepository.save(userMission)
}

@Transactional
fun failIfNoRetroLastWeek(userMission: UserMission) {
val lastWeekStart =
ServiceTime.today().with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).minusWeeks(1)

val lastWeekRetroCount =
retrospectiveRepository.countCompletedByUserIdAndPeriod(
userId = userMission.userId,
from = ServiceTime.startOfDayUtc(lastWeekStart),
to = ServiceTime.startOfDayUtc(lastWeekStart.plusWeeks(1)),
)

if (lastWeekRetroCount == 0) {
userMission.setFailureWaitingConfirm()
userMissionRepository.save(userMission)
logger.info("연속주차 실패 - userId: ${userMission.userId}, missionId: ${userMission.missionId}")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,61 +1,40 @@
package com.didit.adapter.integration.scheduler

import com.didit.application.achievement.required.MissionRepository
import com.didit.application.achievement.required.UserMissionRepository
import com.didit.application.retrospect.required.RetrospectiveRepository
import com.didit.domain.shared.ServiceTime
import jakarta.transaction.Transactional
import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import java.time.temporal.TemporalAdjusters

@Component
class MissionScheduler(
private val userMissionRepository: UserMissionRepository,
private val missionRepository: MissionRepository,
private val retrospectiveRepository: RetrospectiveRepository,
private val missionFailureProcessor: MissionFailureProcessor,
) {
companion object {
private val logger = LoggerFactory.getLogger(MissionScheduler::class.java)
}

@Scheduled(cron = "0 0 * * * *")
@Transactional
fun checkExpiredLv2Missions() {
val expiredMissions = userMissionRepository.findExpiredLv2Missions()

var count = 0
expiredMissions.forEach { userMission ->
userMission.setFailureWaitingConfirm()
userMissionRepository.save(userMission)
runCatching { missionFailureProcessor.failExpiredLv2(userMission) }
.onSuccess { count++ }
.onFailure { logger.error("Lv2 만료 미션 처리 실패 - missionId: ${userMission.id}", it) }
}

logger.info("Lv2 만료 미션 검사 완료 - count: ${expiredMissions.size}")
logger.info("Lv2 만료 미션 검사 완료 - count: $count")
}

@Scheduled(cron = "0 0 0 ? * MON")
@Transactional
fun checkConsecutiveWeekFailure() {
val consecutiveWeekMissions = userMissionRepository.findConsecutiveWeekMissionsInProgress()

consecutiveWeekMissions.forEach { userMission ->
val lastWeekStart =
ServiceTime.today().with(TemporalAdjusters.previousOrSame(java.time.DayOfWeek.MONDAY)).minusWeeks(1)

val lastWeekRetroCount =
retrospectiveRepository.countCompletedByUserIdAndPeriod(
userId = userMission.userId,
from = ServiceTime.startOfDayUtc(lastWeekStart),
to = ServiceTime.startOfDayUtc(lastWeekStart.plusWeeks(1)),
)

if (lastWeekRetroCount == 0) {
userMission.setFailureWaitingConfirm()
userMissionRepository.save(userMission)
logger.info(
"연속주차 실패 - userId: ${userMission.userId}, missionId: ${userMission.missionId}",
)
}
runCatching { missionFailureProcessor.failIfNoRetroLastWeek(userMission) }
.onFailure { logger.error("연속주차 실패 검사 실패 - missionId: ${userMission.id}", it) }
}

logger.info("연속주차 실패 검사 완료 - total: ${consecutiveWeekMissions.size}")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
package com.didit.adapter.integration.scheduler

import com.didit.application.notification.provided.NotificationHistoryRegister
import com.didit.application.notification.provided.NotificationSettingFinder
import com.didit.application.notification.provided.UserPushSender
import com.didit.domain.notification.NotificationHistoryCreateRequest
import com.didit.domain.notification.NotificationType
import jakarta.transaction.Transactional
import com.didit.domain.shared.ServiceTime
import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import java.time.LocalTime
import java.time.ZoneId
import java.util.UUID

@Component
class NotificationScheduler(
private val notificationSettingFinder: NotificationSettingFinder,
private val notificationHistoryRegister: NotificationHistoryRegister,
private val userPushSender: UserPushSender,
private val reminderNotificationSender: ReminderNotificationSender,
) {
companion object {
private val logger = LoggerFactory.getLogger(NotificationScheduler::class.java)
Expand All @@ -28,31 +21,15 @@ class NotificationScheduler(
}

@Scheduled(cron = "0 */10 * * * *")
@Transactional
fun sendReminderNotifications() {
val now = LocalTime.now(ZoneId.of("Asia/Seoul")).withSecond(0).withNano(0)
val now = LocalTime.now(ServiceTime.ZONE).withSecond(0).withNano(0)
val settings = notificationSettingFinder.findAllByReminderTime(now)

logger.info("회고 알림 스케줄러 실행 - time: $now, 대상 유저 수: ${settings.size}")

settings.forEach { setting ->
runCatching {
sendToUser(setting.userId)
}.onFailure { e ->
logger.error("회고 알림 전송 실패 - userId: ${setting.userId}", e)
}
runCatching { reminderNotificationSender.send(setting.userId) }
.onFailure { e -> logger.error("회고 알림 전송 실패 - userId: ${setting.userId}", e) }
}
}

private fun sendToUser(userId: UUID) {
notificationHistoryRegister.save(
NotificationHistoryCreateRequest(
userId = userId,
type = NotificationType.DAILY_REMINDER,
title = DAILY_REMINDER_TITLE,
body = DAILY_REMINDER_BODY,
),
)
userPushSender.sendToUser(userId, DAILY_REMINDER_TITLE, DAILY_REMINDER_BODY, DAILY_REMINDER_LINK)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.didit.adapter.integration.scheduler

import com.didit.application.notification.provided.NotificationHistoryRegister
import com.didit.application.notification.provided.UserPushSender
import com.didit.domain.notification.NotificationHistoryCreateRequest
import com.didit.domain.notification.NotificationType
import jakarta.transaction.Transactional
import org.springframework.stereotype.Component
import java.util.UUID

@Component
class ReminderNotificationSender(
private val notificationHistoryRegister: NotificationHistoryRegister,
private val userPushSender: UserPushSender,
) {
@Transactional
fun send(userId: UUID) {
notificationHistoryRegister.save(
NotificationHistoryCreateRequest(
userId = userId,
type = NotificationType.DAILY_REMINDER,
title = NotificationScheduler.DAILY_REMINDER_TITLE,
body = NotificationScheduler.DAILY_REMINDER_BODY,
),
)
userPushSender.sendToUser(
userId,
NotificationScheduler.DAILY_REMINDER_TITLE,
NotificationScheduler.DAILY_REMINDER_BODY,
NotificationScheduler.DAILY_REMINDER_LINK,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.didit.adapter.persistence.achievement

import com.didit.application.achievement.provided.AchievementDeletionPort
import com.didit.application.achievement.required.DailyAccessStreakRepository
import com.didit.application.achievement.required.UserBadgeRepository
import com.didit.application.achievement.required.UserLevelRepository
import com.didit.application.achievement.required.UserMissionRepository
import com.didit.application.achievement.required.WeeklyRetroStreakRepository
import org.springframework.stereotype.Component
import java.util.UUID

@Component
class AchievementDeletionAdapter(
private val userBadgeRepository: UserBadgeRepository,
private val userMissionRepository: UserMissionRepository,
private val userLevelRepository: UserLevelRepository,
private val weeklyRetroStreakRepository: WeeklyRetroStreakRepository,
private val dailyAccessStreakRepository: DailyAccessStreakRepository,
) : AchievementDeletionPort {
override fun deleteByUserId(userId: UUID) {
userBadgeRepository.deleteAllByUserId(userId)
userMissionRepository.deleteAllByUserId(userId)
userLevelRepository.deleteAllByUserId(userId)
weeklyRetroStreakRepository.deleteAllByUserId(userId)
dailyAccessStreakRepository.deleteAllByUserId(userId)
}
}
Loading
Loading