diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserIdResponse.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserIdResponse.kt index 2432fe93..358bcd4d 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserIdResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserIdResponse.kt @@ -1,7 +1,7 @@ package org.yapp.apis.auth.dto.response import io.swagger.v3.oas.annotations.media.Schema -import org.yapp.domain.token.RefreshToken.UserId +import org.yapp.domain.user.User import java.util.* @Schema( @@ -13,7 +13,7 @@ data class UserIdResponse( val userId: UUID ) { companion object { - fun from(userId: UserId): UserIdResponse { + fun from(userId: User.Id): UserIdResponse { return UserIdResponse(userId.value) } } diff --git a/apis/src/main/kotlin/org/yapp/apis/user/controller/UserController.kt b/apis/src/main/kotlin/org/yapp/apis/user/controller/UserController.kt index 5a8b6c84..b4235a0f 100644 --- a/apis/src/main/kotlin/org/yapp/apis/user/controller/UserController.kt +++ b/apis/src/main/kotlin/org/yapp/apis/user/controller/UserController.kt @@ -4,7 +4,7 @@ import jakarta.validation.Valid import org.springframework.http.ResponseEntity import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.* -import org.yapp.apis.user.dto.request.FcmTokenRequest +import org.yapp.apis.user.dto.request.DeviceRequest import org.yapp.apis.user.dto.request.NotificationSettingsRequest import org.yapp.apis.user.dto.request.TermsAgreementRequest import org.yapp.apis.user.dto.response.UserProfileResponse @@ -42,12 +42,12 @@ class UserController( return ResponseEntity.ok(userProfile) } - @PutMapping("/me/fcm-token") - override fun updateFcmToken( + @PutMapping("/me/devices") + override fun registerDevice( @AuthenticationPrincipal userId: UUID, - @Valid @RequestBody request: FcmTokenRequest + @Valid @RequestBody request: DeviceRequest ): ResponseEntity { - val userProfile = userUseCase.updateFcmToken(userId, request) + val userProfile = userUseCase.registerDevice(userId, request) return ResponseEntity.ok(userProfile) } } diff --git a/apis/src/main/kotlin/org/yapp/apis/user/controller/UserControllerApi.kt b/apis/src/main/kotlin/org/yapp/apis/user/controller/UserControllerApi.kt index 88447bd4..e9849d01 100644 --- a/apis/src/main/kotlin/org/yapp/apis/user/controller/UserControllerApi.kt +++ b/apis/src/main/kotlin/org/yapp/apis/user/controller/UserControllerApi.kt @@ -14,7 +14,7 @@ import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping -import org.yapp.apis.user.dto.request.FcmTokenRequest +import org.yapp.apis.user.dto.request.DeviceRequest import org.yapp.apis.user.dto.request.NotificationSettingsRequest import org.yapp.apis.user.dto.request.TermsAgreementRequest import org.yapp.apis.user.dto.response.UserProfileResponse @@ -122,14 +122,14 @@ interface UserControllerApi { ): ResponseEntity @Operation( - summary = "FCM 토큰 등록", - description = "사용자의 FCM 토큰을 등록합니다." + summary = "디바이스 등록", + description = "사용자의 디바이스를 등록합니다. 이미 등록된 디바이스인 경우 FCM 토큰을 업데이트합니다." ) @ApiResponses( value = [ ApiResponse( responseCode = "200", - description = "FCM 토큰 등록 성공", + description = "디바이스 등록 또는 업데이트 성공", content = [Content(schema = Schema(implementation = UserProfileResponse::class))] ), ApiResponse( @@ -149,9 +149,9 @@ interface UserControllerApi { ) ] ) - @PutMapping("/me/fcm-token") - fun updateFcmToken( + @PutMapping("/me/devices") + fun registerDevice( @Parameter(hidden = true) @AuthenticationPrincipal userId: UUID, - @Valid @RequestBody @Parameter(description = "FCM 토큰 요청 객체") request: FcmTokenRequest + @Valid @RequestBody @Parameter(description = "디바이스 정보 요청 객체") request: DeviceRequest ): ResponseEntity } diff --git a/apis/src/main/kotlin/org/yapp/apis/user/dto/request/FcmTokenRequest.kt b/apis/src/main/kotlin/org/yapp/apis/user/dto/request/DeviceRequest.kt similarity index 55% rename from apis/src/main/kotlin/org/yapp/apis/user/dto/request/FcmTokenRequest.kt rename to apis/src/main/kotlin/org/yapp/apis/user/dto/request/DeviceRequest.kt index 341e790f..90b5d36f 100644 --- a/apis/src/main/kotlin/org/yapp/apis/user/dto/request/FcmTokenRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/user/dto/request/DeviceRequest.kt @@ -4,10 +4,18 @@ import io.swagger.v3.oas.annotations.media.Schema import jakarta.validation.constraints.NotBlank @Schema( - name = "FcmTokenRequest", - description = "DTO for FCM token update requests" + name = "DeviceRequest", + description = "DTO for device update requests" ) -data class FcmTokenRequest private constructor( +data class DeviceRequest private constructor( + @field:Schema( + description = "디바이스 아이디", + example = "c8a9d7d0-4f6a-4b1a-8f0a-9d8e7f6a4b1a", + required = true + ) + @field:NotBlank(message = "디바이스 아이디는 필수입니다.") + val deviceId: String? = null, + @field:Schema( description = "FCM 토큰", example = "epGzIKlHScicTBrbt26pFG:APA91bG-ZPD-KMJyS-JOiflEPUIVvrp8l9DFBN2dlNG8IHw8mFlkAPok7dVPFJR4phc9061KPztkAIjBJaryZLnv6vIJXNGQsykzDcok3wFC9LrsC-F_aKY", @@ -16,5 +24,6 @@ data class FcmTokenRequest private constructor( @field:NotBlank(message = "FCM 토큰은 필수입니다.") val fcmToken: String? = null ) { + fun validDeviceId(): String = deviceId!!.trim() fun validFcmToken(): String = fcmToken!!.trim() } \ No newline at end of file diff --git a/apis/src/main/kotlin/org/yapp/apis/user/service/UserService.kt b/apis/src/main/kotlin/org/yapp/apis/user/service/UserService.kt index 0f337672..43c8eff8 100644 --- a/apis/src/main/kotlin/org/yapp/apis/user/service/UserService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/user/service/UserService.kt @@ -3,19 +3,21 @@ package org.yapp.apis.user.service import jakarta.validation.Valid import org.yapp.apis.auth.exception.AuthErrorCode import org.yapp.apis.auth.exception.AuthException -import org.yapp.apis.user.dto.request.FcmTokenRequest +import org.yapp.apis.user.dto.request.DeviceRequest import org.yapp.apis.user.dto.request.FindUserIdentityRequest import org.yapp.apis.user.dto.request.NotificationSettingsRequest import org.yapp.apis.user.dto.request.TermsAgreementRequest import org.yapp.apis.user.dto.response.UserAuthInfoResponse import org.yapp.apis.user.dto.response.UserProfileResponse +import org.yapp.domain.device.DeviceDomainService import org.yapp.domain.user.UserDomainService import org.yapp.globalutils.annotation.ApplicationService import java.util.* @ApplicationService class UserService( - private val userDomainService: UserDomainService + private val userDomainService: UserDomainService, + private val deviceDomainService: DeviceDomainService ) { fun findUserProfileByUserId(userId: UUID): UserProfileResponse { val userProfile = userDomainService.findUserProfileById(userId) @@ -45,9 +47,10 @@ class UserService( return UserProfileResponse.from(updatedUserProfile) } - fun updateFcmToken(userId: UUID, @Valid request: FcmTokenRequest): UserProfileResponse { + fun registerDevice(userId: UUID, @Valid request: DeviceRequest): UserProfileResponse { validateUserExists(userId) - val updatedUserProfile = userDomainService.updateFcmToken(userId, request.validFcmToken()) - return UserProfileResponse.from(updatedUserProfile) + deviceDomainService.findOrCreateDevice(userId, request.validDeviceId(), request.validFcmToken()) + val userProfile = userDomainService.findUserProfileById(userId) + return UserProfileResponse.from(userProfile) } } diff --git a/apis/src/main/kotlin/org/yapp/apis/user/usecase/UserUseCase.kt b/apis/src/main/kotlin/org/yapp/apis/user/usecase/UserUseCase.kt index 5e552bd1..b4e65cdf 100644 --- a/apis/src/main/kotlin/org/yapp/apis/user/usecase/UserUseCase.kt +++ b/apis/src/main/kotlin/org/yapp/apis/user/usecase/UserUseCase.kt @@ -1,7 +1,7 @@ package org.yapp.apis.user.usecase import org.springframework.transaction.annotation.Transactional -import org.yapp.apis.user.dto.request.FcmTokenRequest +import org.yapp.apis.user.dto.request.DeviceRequest import org.yapp.apis.user.dto.request.NotificationSettingsRequest import org.yapp.apis.user.dto.request.TermsAgreementRequest import org.yapp.apis.user.dto.response.UserProfileResponse @@ -29,7 +29,7 @@ class UserUseCase( } @Transactional - fun updateFcmToken(userId: UUID, request: FcmTokenRequest): UserProfileResponse { - return userService.updateFcmToken(userId, request) + fun registerDevice(userId: UUID, request: DeviceRequest): UserProfileResponse { + return userService.registerDevice(userId, request) } } diff --git a/batch/src/main/kotlin/org/yapp/batch/config/InfraConfig.kt b/batch/src/main/kotlin/org/yapp/batch/config/InfraConfig.kt index 44294679..d521da40 100644 --- a/batch/src/main/kotlin/org/yapp/batch/config/InfraConfig.kt +++ b/batch/src/main/kotlin/org/yapp/batch/config/InfraConfig.kt @@ -7,8 +7,9 @@ import org.yapp.infra.InfraBaseConfigGroup @Configuration(proxyBeanMethods = false) @EnableInfraBaseConfig( [ - InfraBaseConfigGroup.JPA + InfraBaseConfigGroup.JPA, + InfraBaseConfigGroup.AOP, + InfraBaseConfigGroup.SENTRY ] ) -class InfraConfig { -} +class InfraConfig diff --git a/batch/src/main/kotlin/org/yapp/batch/job/fcm/FcmNotificationJobConfig.kt b/batch/src/main/kotlin/org/yapp/batch/job/fcm/FcmNotificationJobConfig.kt index 637eed6a..cdea77a5 100644 --- a/batch/src/main/kotlin/org/yapp/batch/job/fcm/FcmNotificationJobConfig.kt +++ b/batch/src/main/kotlin/org/yapp/batch/job/fcm/FcmNotificationJobConfig.kt @@ -4,220 +4,33 @@ import org.slf4j.LoggerFactory import org.springframework.context.annotation.Configuration import org.springframework.scheduling.annotation.EnableScheduling import org.springframework.scheduling.annotation.Scheduled -import org.springframework.transaction.annotation.Transactional -import org.yapp.batch.service.FcmService -import org.yapp.domain.notification.Notification -import org.yapp.domain.notification.NotificationRepository -import org.yapp.domain.notification.NotificationType -import org.yapp.domain.user.User -import org.yapp.domain.user.UserRepository -import java.time.LocalDateTime -import java.util.UUID +import org.yapp.batch.service.NotificationService @Configuration @EnableScheduling class FcmNotificationJobConfig( - private val userRepository: UserRepository, - private val notificationRepository: NotificationRepository, - private val fcmService: FcmService + private val notificationService: NotificationService ) { private val logger = LoggerFactory.getLogger(FcmNotificationJobConfig::class.java) - @Scheduled(fixedRate = 60000) - @Transactional - fun checkAndSendNotifications() { - logger.info("Starting notification check job") - - val sevenDaysAgo = LocalDateTime.now().minusDays(7) - val unrecordedUsers = userRepository.findByLastActivityBeforeAndNotificationEnabled(sevenDaysAgo, true) - - val userNotificationsCache = mutableMapOf>() - - var unrecordedSuccessCount = 0 - unrecordedUsers.forEach { user -> - val userNotifications = userNotificationsCache.getOrPut(user.id.value) { - notificationRepository.findByUserId(user.id.value) - } - - val hasActiveUnrecordedNotification = userNotifications.any { - it.notificationType == NotificationType.UNRECORDED && it.isSent - } - if (!hasActiveUnrecordedNotification) { - if (userNotifications.isNotEmpty()) { - val resetNotifications = userNotifications.filter { - it.notificationType == NotificationType.UNRECORDED && !it.isSent - } - if (resetNotifications.isNotEmpty()) { - logger.info("Sending new unrecorded notification to user ${user.id.value} after previous notification was reset. User lastActivity: ${user.lastActivity}") - } - } - val (success, updatedNotifications) = sendNotificationToUser( - user = user, - title = "\uD83D\uDCDA 잠시 멈춘 기록.. 다시 이어가 볼까요?", - message = "이번주에 읽은 책, 잊기 전에 기록해 보세요!", - notificationType = NotificationType.UNRECORDED, - existingNotifications = userNotifications - ) - if (success) unrecordedSuccessCount++ - if (updatedNotifications.isNotEmpty()) { - userNotificationsCache[user.id.value] = updatedNotifications - } - } - } - val thirtyDaysAgo = LocalDateTime.now().minusDays(30) - val dormantUsers = userRepository.findByLastActivityBeforeAndNotificationEnabled(thirtyDaysAgo, true) - - var dormantSuccessCount = 0 - dormantUsers.forEach { user -> - val userNotifications = userNotificationsCache.getOrPut(user.id.value) { - notificationRepository.findByUserId(user.id.value) - } - - val hasActiveDormantNotification = userNotifications.any { - it.notificationType == NotificationType.DORMANT && it.isSent - } - if (!hasActiveDormantNotification) { - if (userNotifications.isNotEmpty()) { - val resetNotifications = userNotifications.filter { - it.notificationType == NotificationType.DORMANT && !it.isSent - } - if (resetNotifications.isNotEmpty()) { - logger.info("Sending new dormant notification to user ${user.id.value} after previous notification was reset. User lastActivity: ${user.lastActivity}") - } - } - val (success, updatedNotifications) = sendNotificationToUser( - user = user, - title = "\uD83D\uDCDA Reed와 함께 독서 기록 시작", - message = "그동안 읽은 책을 모아 기록해 보세요!", - notificationType = NotificationType.DORMANT, - existingNotifications = userNotifications - ) - if (success) dormantSuccessCount++ - if (updatedNotifications.isNotEmpty()) { - userNotificationsCache[user.id.value] = updatedNotifications - } - } - } - resetNotificationsForActiveUsers() - - logger.info("Completed notification check job. Successfully sent unrecorded notifications to $unrecordedSuccessCount out of ${unrecordedUsers.size} users and dormant notifications to $dormantSuccessCount out of ${dormantUsers.size} users") - } - - @Transactional - protected fun resetNotificationsForActiveUsers() { - val sentNotifications = notificationRepository.findBySent(true) - - sentNotifications.forEach { notification -> - val user = notification.user - val sentAt = notification.sentAt - val lastActivity = user.lastActivity - - if (sentAt != null && lastActivity != null && lastActivity.isAfter(sentAt)) { - val updatedNotification = Notification.reconstruct( - id = notification.id, - user = user, - fcmToken = notification.fcmToken, - title = notification.title, - message = notification.message, - notificationType = notification.notificationType, - isRead = notification.isRead, - isSent = false, - sentAt = null, - createdAt = notification.createdAt, - updatedAt = notification.updatedAt - ) - - notificationRepository.save(updatedNotification) - logger.info("Reset notification status for user ${user.id.value} as they have been active since the notification was sent (lastActivity: $lastActivity, sentAt: $sentAt)") - } - } + companion object { + private const val UNRECORDED_DAYS_THRESHOLD = 7 + private const val DORMANT_DAYS_THRESHOLD = 30 } - @Transactional - protected fun sendNotificationToUser( - user: User, - title: String, - message: String, - notificationType: NotificationType, - existingNotifications: List = emptyList() - ): Pair> { - try { - val userNotifications = if (existingNotifications.isNotEmpty()) { - existingNotifications.toMutableList() - } else { - notificationRepository.findByUserId(user.id.value).toMutableList() - } - - val existingNotification = userNotifications.find { it.notificationType == notificationType } - - val fcmTokens = userNotifications.map { it.fcmToken }.filter { it.isNotBlank() } - - if (fcmTokens.isEmpty()) { - if (existingNotification != null) { - logger.info("No valid FCM tokens found for user: ${user.id.value}, using existing notification") - return Pair(false, userNotifications) - } - - val notification = Notification.create( - user = user, - title = title, - message = message, - notificationType = notificationType, - isSent = false, - sentAt = null - ) - - val savedNotification = notificationRepository.save(notification) - userNotifications.add(savedNotification) - logger.info("No FCM token found for user: ${user.id.value}, notification saved to database only") - return Pair(false, userNotifications) - } - - val results = fcmService.sendMulticastNotification(fcmTokens, title, message) - logger.info("Sent notifications to user ${user.id.value}: ${results.size} successful out of ${fcmTokens.size}") - - if (results.isNotEmpty()) { - if (existingNotification != null) { - val updatedNotification = Notification.reconstruct( - id = existingNotification.id, - user = user, - fcmToken = existingNotification.fcmToken, - title = title, - message = message, - notificationType = notificationType, - isRead = existingNotification.isRead, - isSent = true, - sentAt = LocalDateTime.now(), - createdAt = existingNotification.createdAt, - updatedAt = existingNotification.updatedAt - ) - - val savedNotification = notificationRepository.save(updatedNotification) - val index = userNotifications.indexOfFirst { it.id == existingNotification.id } - if (index >= 0) { - userNotifications[index] = savedNotification - } - logger.info("Updated existing notification for user ${user.id.value} to sent") - } else { - val notification = Notification.create( - user = user, - title = title, - message = message, - notificationType = notificationType, - isSent = true, - sentAt = LocalDateTime.now() - ) - - val savedNotification = notificationRepository.save(notification) - userNotifications.add(savedNotification) - logger.info("Created new notification for user ${user.id.value} with sent status") - } - } - - return Pair(results.isNotEmpty(), userNotifications) - } catch (e: Exception) { - logger.error("Error sending notification to user ${user.id.value}", e) - return Pair(false, existingNotifications) - } + @Scheduled(fixedRate = 60000) + fun checkAndSendNotifications() { + logger.info("========== Starting FCM notification job ==========") + + val (unrecordedUserCount, unrecordedDeviceCount) = notificationService.sendUnrecordedNotifications(UNRECORDED_DAYS_THRESHOLD) + val (dormantUserCount, dormantDeviceCount) = notificationService.sendDormantNotifications(DORMANT_DAYS_THRESHOLD) + notificationService.resetNotificationsForActiveUsers() + + logger.info( + "========== Completed FCM notification job ========== \n" + + "Summary:\n" + + " - Unrecorded: $unrecordedUserCount users, $unrecordedDeviceCount devices\n" + + " - Dormant: $dormantUserCount users, $dormantDeviceCount devices" + ) } } diff --git a/batch/src/main/kotlin/org/yapp/batch/service/NotificationService.kt b/batch/src/main/kotlin/org/yapp/batch/service/NotificationService.kt new file mode 100644 index 00000000..13a9b393 --- /dev/null +++ b/batch/src/main/kotlin/org/yapp/batch/service/NotificationService.kt @@ -0,0 +1,161 @@ +package org.yapp.batch.service + +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.yapp.domain.device.DeviceDomainService +import org.yapp.domain.device.vo.DeviceVO +import org.yapp.domain.notification.NotificationDomainService +import org.yapp.domain.notification.NotificationType +import org.yapp.domain.user.User +import org.yapp.domain.user.UserDomainService +import org.yapp.domain.user.vo.NotificationTargetUserVO + +@Service +class NotificationService( + private val userDomainService: UserDomainService, + private val notificationDomainService: NotificationDomainService, + private val deviceDomainService: DeviceDomainService, + private val fcmService: FcmService +) { + private val logger = LoggerFactory.getLogger(NotificationService::class.java) + + companion object { + private const val UNRECORDED_NOTIFICATION_TITLE = "📚 잠시 멈춘 기록.. 다시 이어가 볼까요?" + private const val UNRECORDED_NOTIFICATION_MESSAGE = "이번주에 읽은 책, 잊기 전에 기록해 보세요!" + private const val DORMANT_NOTIFICATION_TITLE = "📚 Reed와 함께 독서 기록 시작" + private const val DORMANT_NOTIFICATION_MESSAGE = "그동안 읽은 책을 모아 기록해 보세요!" + } + + @Transactional + fun sendUnrecordedNotifications(daysThreshold: Int): Pair { + return sendNotificationsByType( + daysThreshold = daysThreshold, + notificationType = NotificationType.UNRECORDED, + title = UNRECORDED_NOTIFICATION_TITLE, + message = UNRECORDED_NOTIFICATION_MESSAGE, + findUsers = { userDomainService.findUnrecordedUsers(it) } + ) + } + + @Transactional + fun sendDormantNotifications(daysThreshold: Int): Pair { + return sendNotificationsByType( + daysThreshold = daysThreshold, + notificationType = NotificationType.DORMANT, + title = DORMANT_NOTIFICATION_TITLE, + message = DORMANT_NOTIFICATION_MESSAGE, + findUsers = { userDomainService.findDormantUsers(it) } + ) + } + + private fun sendNotificationsByType( + daysThreshold: Int, + notificationType: NotificationType, + title: String, + message: String, + findUsers: (Int) -> List + ): Pair { + logger.info("Starting $notificationType notifications (threshold: $daysThreshold days)") + + val users = findUsers(daysThreshold) + logger.info("Found ${users.size} $notificationType users") + + var successUserCount = 0 + var successDeviceCount = 0 + + users.forEach { user -> + val (success, deviceCount) = sendNotificationsToUser( + user = user, + title = title, + message = message, + notificationType = notificationType + ) + + if (success) { + successUserCount++ + successDeviceCount += deviceCount + } + } + + logger.info("Completed $notificationType notifications: $successUserCount users, $successDeviceCount devices") + return Pair(successUserCount, successDeviceCount) + } + + private fun sendNotificationsToUser( + user: NotificationTargetUserVO, + title: String, + message: String, + notificationType: NotificationType + ): Pair { + val userId = User.Id.newInstance(user.id) + if (notificationDomainService.hasActiveNotification(userId, notificationType)) { + logger.info("User ${user.id} already has active $notificationType notification, skipping") + return Pair(false, 0) + } + + val devices = deviceDomainService.findDevicesByUserId(user.id) + if (devices.isEmpty()) { + logger.info("No devices found for user ${user.id}") + return Pair(false, 0) + } + + val successDeviceCount = sendToDevices(devices, title, message) + if (successDeviceCount > 0) { + notificationDomainService.createAndSaveNotification( + userId = userId, + title = title, + message = message, + notificationType = notificationType + ) + return Pair(true, successDeviceCount) + } + + logger.info("Failed to send notification to any device for user ${user.id}") + return Pair(false, 0) + } + + private fun sendToDevices( + devices: List, + title: String, + message: String + ): Int { + val validTokens = devices + .filter { it.fcmToken.isNotBlank() } + .map { it.fcmToken } + + if (validTokens.isEmpty()) { + return 0 + } + + return try { + val results = fcmService.sendMulticastNotification(validTokens, title, message) + results.size + } catch (e: Exception) { + logger.error("Error sending notifications to devices", e) + 0 + } + } + + @Transactional + fun resetNotificationsForActiveUsers() { + val sentNotifications = notificationDomainService.findSentNotifications() + + sentNotifications.forEach { notification -> + val sentAt = notification.sentAt + if (sentAt != null) { + try { + val user = userDomainService.findNotificationTargetUserById(notification.userId.value) + val lastActivity = user.lastActivity + + if (lastActivity != null && lastActivity.isAfter(sentAt)) { + val resetNotification = notification.reset() + notificationDomainService.save(resetNotification) + } + } catch (e: Exception) { + logger.warn("Failed to reset notification for user ${notification.userId.value}", e) + } + } + } + } +} diff --git a/domain/src/main/kotlin/org/yapp/domain/device/Device.kt b/domain/src/main/kotlin/org/yapp/domain/device/Device.kt new file mode 100644 index 00000000..c5f935f6 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/device/Device.kt @@ -0,0 +1,55 @@ +package org.yapp.domain.device + +import org.yapp.domain.user.User +import org.yapp.globalutils.util.UuidGenerator +import java.time.LocalDateTime +import java.util.UUID + +data class Device private constructor( + val id: Id, + val userId: User.Id, + val deviceId: String, + val fcmToken: String, + val createdAt: LocalDateTime? = null, + val updatedAt: LocalDateTime? = null +) { + companion object { + fun create( + userId: UUID, + deviceId: String, + fcmToken: String + ): Device { + return Device( + id = Id.newInstance(UuidGenerator.create()), + userId = User.Id.newInstance(userId), + deviceId = deviceId, + fcmToken = fcmToken + ) + } + + fun reconstruct( + id: Id, + userId: User.Id, + deviceId: String, + fcmToken: String, + createdAt: LocalDateTime?, + updatedAt: LocalDateTime? + ): Device { + return Device( + id = id, + userId = userId, + deviceId = deviceId, + fcmToken = fcmToken, + createdAt = createdAt, + updatedAt = updatedAt + ) + } + } + + @JvmInline + value class Id(val value: UUID) { + companion object { + fun newInstance(value: UUID) = Id(value) + } + } +} diff --git a/domain/src/main/kotlin/org/yapp/domain/device/DeviceDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/device/DeviceDomainService.kt new file mode 100644 index 00000000..d3fb7484 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/device/DeviceDomainService.kt @@ -0,0 +1,23 @@ +package org.yapp.domain.device + +import org.yapp.domain.device.vo.DeviceVO +import org.yapp.globalutils.annotation.DomainService +import java.util.UUID + +@DomainService +class DeviceDomainService( + private val deviceRepository: DeviceRepository +) { + fun findOrCreateDevice(userId: UUID, deviceId: String, fcmToken: String) { + val device = deviceRepository.findByDeviceId(deviceId) + if (device == null) { + val newDevice = Device.create(userId, deviceId, fcmToken) + deviceRepository.save(newDevice) + } + } + + fun findDevicesByUserId(userId: UUID): List { + return deviceRepository.findByUserId(userId) + .map { DeviceVO.from(it) } + } +} diff --git a/domain/src/main/kotlin/org/yapp/domain/device/DeviceRepository.kt b/domain/src/main/kotlin/org/yapp/domain/device/DeviceRepository.kt new file mode 100644 index 00000000..84186cbe --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/device/DeviceRepository.kt @@ -0,0 +1,9 @@ +package org.yapp.domain.device + +import java.util.UUID + +interface DeviceRepository { + fun findByDeviceId(deviceId: String): Device? + fun save(device: Device): Device + fun findByUserId(userId: UUID): List +} diff --git a/domain/src/main/kotlin/org/yapp/domain/device/vo/DeviceVO.kt b/domain/src/main/kotlin/org/yapp/domain/device/vo/DeviceVO.kt new file mode 100644 index 00000000..b4b16629 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/device/vo/DeviceVO.kt @@ -0,0 +1,20 @@ +package org.yapp.domain.device.vo + +import org.yapp.domain.device.Device +import java.util.UUID + +data class DeviceVO private constructor( + val id: UUID, + val userId: UUID, + val fcmToken: String +) { + companion object { + fun from(device: Device): DeviceVO { + return DeviceVO( + id = device.id.value, + userId = device.userId.value, + fcmToken = device.fcmToken + ) + } + } +} diff --git a/domain/src/main/kotlin/org/yapp/domain/notification/Notification.kt b/domain/src/main/kotlin/org/yapp/domain/notification/Notification.kt index 91c204af..9326e485 100644 --- a/domain/src/main/kotlin/org/yapp/domain/notification/Notification.kt +++ b/domain/src/main/kotlin/org/yapp/domain/notification/Notification.kt @@ -7,8 +7,7 @@ import java.util.UUID data class Notification private constructor( val id: Id, - val user: User, - val fcmToken: String, + val userId: User.Id, val title: String, val message: String, val notificationType: NotificationType, @@ -18,16 +17,16 @@ data class Notification private constructor( val createdAt: LocalDateTime? = null, val updatedAt: LocalDateTime? = null ) { - @JvmInline - value class Id(val value: UUID) { - companion object { - fun newInstance(value: UUID) = Id(value) - } + fun reset(): Notification { + return this.copy( + isSent = false, + sentAt = null + ) } companion object { fun create( - user: User, + userId: UUID, title: String, message: String, notificationType: NotificationType, @@ -36,8 +35,7 @@ data class Notification private constructor( ): Notification { return Notification( id = Id.newInstance(UuidGenerator.create()), - user = user, - fcmToken = findFcmTokenForUser(user), + userId = User.Id.newInstance(userId), title = title, message = message, notificationType = notificationType, @@ -48,8 +46,7 @@ data class Notification private constructor( fun reconstruct( id: Id, - user: User, - fcmToken: String, + userId: User.Id, title: String, message: String, notificationType: NotificationType, @@ -61,8 +58,7 @@ data class Notification private constructor( ): Notification { return Notification( id = id, - user = user, - fcmToken = fcmToken, + userId = userId, title = title, message = message, notificationType = notificationType, @@ -73,15 +69,12 @@ data class Notification private constructor( updatedAt = updatedAt ) } + } - /** - * Retrieves the FCM token from the User object - * - * @param user The user to get the FCM token for - * @return The FCM token, or an empty string if not available - */ - private fun findFcmTokenForUser(user: User): String { - return user.fcmToken ?: "" + @JvmInline + value class Id(val value: UUID) { + companion object { + fun newInstance(value: UUID) = Id(value) } } } diff --git a/domain/src/main/kotlin/org/yapp/domain/notification/NotificationDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/notification/NotificationDomainService.kt new file mode 100644 index 00000000..809fd683 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/notification/NotificationDomainService.kt @@ -0,0 +1,42 @@ +package org.yapp.domain.notification + +import org.yapp.domain.user.User +import org.yapp.globalutils.annotation.DomainService +import java.time.LocalDateTime + +@DomainService +class NotificationDomainService( + private val notificationRepository: NotificationRepository +) { + fun hasActiveNotification(userId: User.Id, notificationType: NotificationType): Boolean { + val userNotifications = notificationRepository.findByUserId(userId.value) + return userNotifications.any { + it.notificationType == notificationType && it.isSent + } + } + + fun createAndSaveNotification( + userId: User.Id, + title: String, + message: String, + notificationType: NotificationType + ) { + val notification = Notification.create( + userId = userId.value, + title = title, + message = message, + notificationType = notificationType, + isSent = true, + sentAt = LocalDateTime.now() + ) + notificationRepository.save(notification) + } + + fun findSentNotifications(): List { + return notificationRepository.findBySent(true) + } + + fun save(notification: Notification) { + notificationRepository.save(notification) + } +} diff --git a/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt index 64de8131..f7cf1cab 100644 --- a/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt @@ -1,12 +1,13 @@ package org.yapp.domain.readingrecord +import org.yapp.domain.userbook.UserBook import org.yapp.globalutils.util.UuidGenerator import java.time.LocalDateTime import java.util.* data class ReadingRecord private constructor( val id: Id, - val userBookId: UserBookId, + val userBookId: UserBook.Id, val pageNumber: PageNumber, val quote: Quote, val review: Review?, @@ -25,7 +26,7 @@ data class ReadingRecord private constructor( ): ReadingRecord { return ReadingRecord( id = Id.newInstance(UuidGenerator.create()), - userBookId = UserBookId.newInstance(userBookId), + userBookId = UserBook.Id.newInstance(userBookId), pageNumber = PageNumber.newInstance(pageNumber), quote = Quote.newInstance(quote), review = Review.newInstance(review), @@ -35,7 +36,7 @@ data class ReadingRecord private constructor( fun reconstruct( id: Id, - userBookId: UserBookId, + userBookId: UserBook.Id, pageNumber: PageNumber, quote: Quote, review: Review?, @@ -80,13 +81,6 @@ data class ReadingRecord private constructor( } } - @JvmInline - value class UserBookId(val value: UUID) { - companion object { - fun newInstance(value: UUID) = UserBookId(value) - } - } - @JvmInline value class PageNumber(val value: Int) { companion object { diff --git a/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt index 8749d8a5..ae1e2a80 100644 --- a/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt @@ -17,7 +17,7 @@ import org.yapp.domain.userbook.exception.UserBookNotFoundException import org.yapp.domain.userbook.exception.UserBookErrorCode @DomainService -class ReadingRecordDomainService( +class ReadingRecordDomainService( // TODO: readingRecordRepository만 남기고 제거 private val readingRecordRepository: ReadingRecordRepository, private val tagRepository: TagRepository, private val readingRecordTagRepository: ReadingRecordTagRepository, diff --git a/domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt index e27d1e90..95126d28 100644 --- a/domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt @@ -1,11 +1,12 @@ package org.yapp.domain.readingrecord.vo import org.yapp.domain.readingrecord.ReadingRecord +import org.yapp.domain.userbook.UserBook import java.time.LocalDateTime data class ReadingRecordInfoVO private constructor( val id: ReadingRecord.Id, - val userBookId: ReadingRecord.UserBookId, + val userBookId: UserBook.Id, val pageNumber: ReadingRecord.PageNumber, val quote: ReadingRecord.Quote, val review: ReadingRecord.Review?, diff --git a/domain/src/main/kotlin/org/yapp/domain/token/RefreshToken.kt b/domain/src/main/kotlin/org/yapp/domain/token/RefreshToken.kt index 87802a0b..82b5a591 100644 --- a/domain/src/main/kotlin/org/yapp/domain/token/RefreshToken.kt +++ b/domain/src/main/kotlin/org/yapp/domain/token/RefreshToken.kt @@ -1,5 +1,6 @@ package org.yapp.domain.token +import org.yapp.domain.user.User import org.yapp.globalutils.util.UuidGenerator import java.time.LocalDateTime import java.util.* @@ -7,7 +8,7 @@ import java.util.* data class RefreshToken private constructor( val id: Id?, val token: Token, - val userId: UserId, + val userId: User.Id, val expiresAt: LocalDateTime, val createdAt: LocalDateTime ) { @@ -25,7 +26,7 @@ data class RefreshToken private constructor( return RefreshToken( id = Id.newInstance(UuidGenerator.create()), token = Token.newInstance(token), - userId = UserId.newInstance(userId), + userId = User.Id.newInstance(userId), expiresAt = expiresAt, createdAt = createdAt ) @@ -34,7 +35,7 @@ data class RefreshToken private constructor( fun reconstruct( id: Id, token: Token, - userId: UserId, + userId: User.Id, expiresAt: LocalDateTime, createdAt: LocalDateTime ): RefreshToken { @@ -70,15 +71,4 @@ data class RefreshToken private constructor( } } } - - @JvmInline - value class UserId(val value: UUID) { - override fun toString(): String = value.toString() - - companion object { - fun newInstance(value: UUID): UserId { - return UserId(value) - } - } - } } diff --git a/domain/src/main/kotlin/org/yapp/domain/token/RefreshTokenDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/token/RefreshTokenDomainService.kt index 75b4bd35..6f7c7c9c 100644 --- a/domain/src/main/kotlin/org/yapp/domain/token/RefreshTokenDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/token/RefreshTokenDomainService.kt @@ -1,9 +1,9 @@ package org.yapp.domain.token import org.yapp.domain.token.RefreshToken.Token -import org.yapp.domain.token.RefreshToken.UserId import org.yapp.domain.token.exception.TokenErrorCode import org.yapp.domain.token.exception.TokenNotFoundException +import org.yapp.domain.user.User import org.yapp.globalutils.annotation.DomainService import java.time.LocalDateTime import java.util.* @@ -37,7 +37,7 @@ class RefreshTokenDomainService( } } - fun getUserIdByToken(refreshToken: String): UserId { + fun getUserIdByToken(refreshToken: String): User.Id { val storedToken = refreshTokenRepository.findByToken(refreshToken) ?: throw TokenNotFoundException(TokenErrorCode.TOKEN_NOT_FOUND) return storedToken.userId diff --git a/domain/src/main/kotlin/org/yapp/domain/user/User.kt b/domain/src/main/kotlin/org/yapp/domain/user/User.kt index 961124f8..f4588c47 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/User.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/User.kt @@ -16,7 +16,6 @@ data class User private constructor( val role: Role, val termsAgreed: Boolean = false, val appleRefreshToken: String? = null, - val fcmToken: String? = null, val notificationEnabled: Boolean = true, val lastActivity: LocalDateTime? = null, val createdAt: LocalDateTime? = null, @@ -43,12 +42,6 @@ data class User private constructor( ) } - fun updateFcmToken(token: String?): User { - return this.copy( - fcmToken = token - ) - } - fun updateNotificationEnabled(enabled: Boolean): User { return this.copy( notificationEnabled = enabled @@ -69,7 +62,6 @@ data class User private constructor( providerType: ProviderType, providerId: String, termsAgreed: Boolean = false, - fcmToken: String? = null, notificationEnabled: Boolean = true ): User { return User( @@ -82,7 +74,6 @@ data class User private constructor( role = Role.USER, termsAgreed = termsAgreed, appleRefreshToken = null, - fcmToken = fcmToken, notificationEnabled = notificationEnabled, lastActivity = LocalDateTime.now() ) @@ -97,7 +88,6 @@ data class User private constructor( providerId: String, role: Role, termsAgreed: Boolean = false, - fcmToken: String? = null, notificationEnabled: Boolean = true ): User { return User( @@ -110,7 +100,6 @@ data class User private constructor( role = role, termsAgreed = termsAgreed, appleRefreshToken = null, - fcmToken = fcmToken, notificationEnabled = notificationEnabled, lastActivity = LocalDateTime.now() ) @@ -126,7 +115,6 @@ data class User private constructor( role: Role, termsAgreed: Boolean = false, appleRefreshToken: String? = null, - fcmToken: String? = null, notificationEnabled: Boolean = true, lastActivity: LocalDateTime? = null, createdAt: LocalDateTime? = null, @@ -143,7 +131,6 @@ data class User private constructor( role = role, termsAgreed = termsAgreed, appleRefreshToken = appleRefreshToken, - fcmToken = fcmToken, notificationEnabled = notificationEnabled, lastActivity = lastActivity, createdAt = createdAt, diff --git a/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt index c67501bd..f22d67e9 100644 --- a/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt @@ -1,15 +1,11 @@ package org.yapp.domain.user +import org.yapp.domain.readingrecord.ReadingRecordRepository import org.yapp.domain.user.exception.UserErrorCode import org.yapp.domain.user.exception.UserNotFoundException -import org.yapp.domain.user.vo.UserAuthVO -import org.yapp.domain.user.vo.UserIdentityVO -import org.yapp.domain.user.vo.UserProfileVO -import org.yapp.domain.user.vo.WithdrawTargetUserVO +import org.yapp.domain.user.vo.* import org.yapp.domain.userbook.UserBookRepository -import org.yapp.domain.readingrecord.ReadingRecordRepository import org.yapp.globalutils.annotation.DomainService - import java.time.LocalDateTime import java.util.* @@ -24,6 +20,11 @@ class UserDomainService( return UserProfileVO.newInstance(user) } + fun findNotificationTargetUserById(id: UUID): NotificationTargetUserVO { + val user = userRepository.findById(id) ?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND) + return NotificationTargetUserVO.from(user) + } + fun findUserIdentityById(id: UUID): UserIdentityVO { val user = userRepository.findById(id) ?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND) return UserIdentityVO.newInstance(user) @@ -105,23 +106,14 @@ class UserDomainService( userRepository.deleteById(user.id.value) } - /** - * Updates the user's lastActivity value if they have registered or recorded a book in the last 7 days. - * If not, the lastActivity value remains unchanged (keeps the login date). - * - * @param userId The user's ID - */ fun updateLastActivity(userId: UUID) { val user = userRepository.findById(userId) ?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND) - // Check if the user has registered or recorded a book in the last 7 days val sevenDaysAgo = LocalDateTime.now().minusDays(7) - // Check for book registrations in the last 7 days val recentBooks = userBookRepository.findByUserIdAndCreatedAtAfter(userId, sevenDaysAgo) - // Check for reading records in the last 7 days val userBooks = userBookRepository.findAllByUserId(userId) val userBookIds = userBooks.map { it.id.value } val recentRecords = if (userBookIds.isNotEmpty()) { @@ -130,19 +122,11 @@ class UserDomainService( emptyList() } - // Update lastActivity only if the user has registered or recorded a book in the last 7 days if (recentBooks.isNotEmpty() || recentRecords.isNotEmpty()) { userRepository.save(user.updateLastActivity()) } } - /** - * Updates the user's notification settings - * - * @param userId The user's ID - * @param notificationEnabled Whether notifications should be enabled - * @return Updated user profile - */ fun updateNotificationSettings(userId: UUID, notificationEnabled: Boolean): UserProfileVO { val user = userRepository.findById(userId) ?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND) @@ -151,18 +135,38 @@ class UserDomainService( return UserProfileVO.newInstance(updatedUser) } - /** - * Updates the user's FCM token - * - * @param userId The user's ID - * @param fcmToken The FCM token - * @return Updated user profile - */ - fun updateFcmToken(userId: UUID, fcmToken: String): UserProfileVO { - val user = userRepository.findById(userId) - ?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND) + fun findUnrecordedUsers(daysThreshold: Int): List { + val targetDate = LocalDateTime.now().minusDays(daysThreshold.toLong()) - val updatedUser = userRepository.save(user.updateFcmToken(fcmToken)) - return UserProfileVO.newInstance(updatedUser) + val allUsers = userRepository.findByLastActivityBeforeAndNotificationEnabled( + LocalDateTime.now().plusDays(1), + true + ) + + return allUsers.filter { user -> + val userId = user.id.value + val recentBooks = userBookRepository.findByUserIdAndCreatedAtAfter(userId, targetDate) + + if (recentBooks.isEmpty()) { + false + } else { + val userBooks = userBookRepository.findAllByUserId(userId) + val userBookIds = userBooks.map { it.id.value } + + val recentRecords = if (userBookIds.isNotEmpty()) { + readingRecordRepository.findByUserBookIdInAndCreatedAtAfter(userBookIds, targetDate) + } else { + emptyList() + } + + recentRecords.isEmpty() + } + }.map { NotificationTargetUserVO.from(it) } + } + + fun findDormantUsers(daysThreshold: Int): List { + val targetDate = LocalDateTime.now().minusDays(daysThreshold.toLong()) + return userRepository.findByLastActivityBeforeAndNotificationEnabled(targetDate, true) + .map { NotificationTargetUserVO.from(it) } } } diff --git a/domain/src/main/kotlin/org/yapp/domain/user/vo/NotificationTargetUserVO.kt b/domain/src/main/kotlin/org/yapp/domain/user/vo/NotificationTargetUserVO.kt new file mode 100644 index 00000000..5b7f17fd --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/user/vo/NotificationTargetUserVO.kt @@ -0,0 +1,23 @@ +package org.yapp.domain.user.vo + +import org.yapp.domain.user.User +import java.time.LocalDateTime +import java.util.UUID + +data class NotificationTargetUserVO private constructor( + val id: UUID, + val email: String, + val nickname: String, + val lastActivity: LocalDateTime? +) { + companion object { + fun from(user: User): NotificationTargetUserVO { + return NotificationTargetUserVO( + id = user.id.value, + email = user.email.value, + nickname = user.nickname, + lastActivity = user.lastActivity + ) + } + } +} diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt index aa6dc0a1..d107264b 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt @@ -1,15 +1,16 @@ package org.yapp.domain.userbook +import org.yapp.domain.book.Book +import org.yapp.domain.user.User import org.yapp.globalutils.util.UuidGenerator -import org.yapp.globalutils.validator.IsbnValidator import java.time.LocalDateTime import java.util.* data class UserBook private constructor( val id: Id, - val userId: UserId, - val bookId: BookId, - val bookIsbn13: BookIsbn13, + val userId: User.Id, + val bookId: Book.Id, + val bookIsbn13: Book.Isbn13, val coverImageUrl: String, val publisher: String, val title: String, @@ -45,9 +46,9 @@ data class UserBook private constructor( ): UserBook { return UserBook( id = Id.newInstance(UuidGenerator.create()), - userId = UserId.newInstance(userId), - bookId = BookId.newInstance(bookId), - bookIsbn13 = BookIsbn13.newInstance(bookIsbn13), + userId = User.Id.newInstance(userId), + bookId = Book.Id.newInstance(bookId), + bookIsbn13 = Book.Isbn13.newInstance(bookIsbn13), coverImageUrl = coverImageUrl, publisher = publisher, title = title, @@ -58,9 +59,9 @@ data class UserBook private constructor( fun reconstruct( id: Id, - userId: UserId, - bookId: BookId, - bookIsbn13: BookIsbn13, + userId: User.Id, + bookId: Book.Id, + bookIsbn13: Book.Isbn13, coverImageUrl: String, publisher: String, title: String, @@ -95,28 +96,4 @@ data class UserBook private constructor( fun newInstance(value: UUID) = Id(value) } } - - @JvmInline - value class UserId(val value: UUID) { - companion object { - fun newInstance(value: UUID) = UserId(value) - } - } - - @JvmInline - value class BookId(val value: UUID) { - companion object { - fun newInstance(value: UUID) = BookId(value) - } - } - - @JvmInline - value class BookIsbn13(val value: String) { - companion object { - fun newInstance(value: String): BookIsbn13 { - require(IsbnValidator.isValidIsbn13(value)) { "ISBN13 must be a 13-digit number." } - return BookIsbn13(value) - } - } - } } diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/vo/HomeBookVO.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/vo/HomeBookVO.kt index 84ba184a..dd9ace90 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/vo/HomeBookVO.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/vo/HomeBookVO.kt @@ -1,14 +1,16 @@ package org.yapp.domain.userbook.vo +import org.yapp.domain.book.Book +import org.yapp.domain.user.User import org.yapp.domain.userbook.BookStatus import org.yapp.domain.userbook.UserBook import java.time.LocalDateTime data class HomeBookVO private constructor( val id: UserBook.Id, - val userId: UserBook.UserId, - val bookId: UserBook.BookId, - val bookIsbn13: UserBook.BookIsbn13, + val userId: User.Id, + val bookId: Book.Id, + val bookIsbn13: Book.Isbn13, val coverImageUrl: String, val publisher: String, val title: String, diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt index c5e7647b..415714d8 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt @@ -1,14 +1,16 @@ package org.yapp.domain.userbook.vo +import org.yapp.domain.book.Book +import org.yapp.domain.user.User import org.yapp.domain.userbook.BookStatus import org.yapp.domain.userbook.UserBook import java.time.LocalDateTime data class UserBookInfoVO private constructor( val id: UserBook.Id, - val userId: UserBook.UserId, - val bookId: UserBook.BookId, - val bookIsbn13: UserBook.BookIsbn13, + val userId: User.Id, + val bookId: Book.Id, + val bookIsbn13: Book.Isbn13, val coverImageUrl: String, val publisher: String, val title: String, diff --git a/infra/src/main/kotlin/org/yapp/infra/device/DeviceJpaRepository.kt b/infra/src/main/kotlin/org/yapp/infra/device/DeviceJpaRepository.kt new file mode 100644 index 00000000..5e2bacc1 --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/device/DeviceJpaRepository.kt @@ -0,0 +1,10 @@ +package org.yapp.infra.device + +import org.springframework.data.jpa.repository.JpaRepository +import org.yapp.infra.device.entity.DeviceEntity +import java.util.UUID + +interface DeviceJpaRepository : JpaRepository { + fun findByDeviceId(deviceId: String): DeviceEntity? + fun findByUserId(userId: UUID): List +} diff --git a/infra/src/main/kotlin/org/yapp/infra/device/DeviceRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/device/DeviceRepositoryImpl.kt new file mode 100644 index 00000000..a27f17dd --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/device/DeviceRepositoryImpl.kt @@ -0,0 +1,24 @@ +package org.yapp.infra.device + +import org.springframework.stereotype.Repository +import org.yapp.domain.device.Device +import org.yapp.domain.device.DeviceRepository +import org.yapp.infra.device.entity.DeviceEntity +import java.util.UUID + +@Repository +class DeviceRepositoryImpl( + private val deviceJpaRepository: DeviceJpaRepository +) : DeviceRepository { + override fun findByDeviceId(deviceId: String): Device? { + return deviceJpaRepository.findByDeviceId(deviceId)?.toDomain() + } + + override fun save(device: Device): Device { + return deviceJpaRepository.save(DeviceEntity.fromDomain(device)).toDomain() + } + + override fun findByUserId(userId: UUID): List { + return deviceJpaRepository.findByUserId(userId).map { it.toDomain() } + } +} diff --git a/infra/src/main/kotlin/org/yapp/infra/device/entity/DeviceEntity.kt b/infra/src/main/kotlin/org/yapp/infra/device/entity/DeviceEntity.kt new file mode 100644 index 00000000..91426181 --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/device/entity/DeviceEntity.kt @@ -0,0 +1,51 @@ +package org.yapp.infra.device.entity + +import jakarta.persistence.* +import org.hibernate.annotations.JdbcTypeCode +import org.yapp.domain.device.Device +import org.yapp.domain.user.User +import org.yapp.infra.common.BaseTimeEntity +import java.sql.Types +import java.util.UUID + +@Entity +@Table(name = "device") +class DeviceEntity( + @Id + @JdbcTypeCode(Types.VARCHAR) + @Column(length = 36, updatable = false, nullable = false) + val id: UUID, + + @JdbcTypeCode(Types.VARCHAR) + @Column(name = "user_id", length = 36, nullable = false) + val userId: UUID, + + @Column(name = "device_id", nullable = false) + var deviceId: String, + + @Column(name = "fcm_token", nullable = false) + var fcmToken: String, +) : BaseTimeEntity() { + + fun toDomain(): Device { + return Device.reconstruct( + id = Device.Id.newInstance(this.id), + userId = User.Id.newInstance(this.userId), + deviceId = this.deviceId, + fcmToken = this.fcmToken, + createdAt = this.createdAt, + updatedAt = this.updatedAt + ) + } + + companion object { + fun fromDomain(device: Device): DeviceEntity { + return DeviceEntity( + id = device.id.value, + userId = device.userId.value, + deviceId = device.deviceId, + fcmToken = device.fcmToken + ) + } + } +} diff --git a/infra/src/main/kotlin/org/yapp/infra/external/redis/entity/RefreshTokenEntity.kt b/infra/src/main/kotlin/org/yapp/infra/external/redis/entity/RefreshTokenEntity.kt index 3fea9c13..f9bb0000 100644 --- a/infra/src/main/kotlin/org/yapp/infra/external/redis/entity/RefreshTokenEntity.kt +++ b/infra/src/main/kotlin/org/yapp/infra/external/redis/entity/RefreshTokenEntity.kt @@ -4,6 +4,7 @@ import org.springframework.data.annotation.Id import org.springframework.data.redis.core.RedisHash import org.springframework.data.redis.core.index.Indexed import org.yapp.domain.token.RefreshToken +import org.yapp.domain.user.User import org.yapp.globalutils.util.UuidGenerator import java.time.LocalDateTime import java.util.* @@ -25,7 +26,7 @@ class RefreshTokenEntity private constructor( fun toDomain(): RefreshToken = RefreshToken.reconstruct( id = RefreshToken.Id.newInstance(this.id), token = RefreshToken.Token.newInstance(this.token), - userId = RefreshToken.UserId.newInstance(this.userId), + userId = User.Id.newInstance(this.userId), expiresAt = expiresAt, createdAt = createdAt ) diff --git a/infra/src/main/kotlin/org/yapp/infra/notification/entity/NotificationEntity.kt b/infra/src/main/kotlin/org/yapp/infra/notification/entity/NotificationEntity.kt index b3abc5e0..6953c0b3 100644 --- a/infra/src/main/kotlin/org/yapp/infra/notification/entity/NotificationEntity.kt +++ b/infra/src/main/kotlin/org/yapp/infra/notification/entity/NotificationEntity.kt @@ -4,8 +4,8 @@ import jakarta.persistence.* import org.hibernate.annotations.JdbcTypeCode import org.yapp.domain.notification.Notification import org.yapp.domain.notification.NotificationType +import org.yapp.domain.user.User import org.yapp.infra.common.BaseTimeEntity -import org.yapp.infra.user.entity.UserEntity import java.sql.Types import java.time.LocalDateTime import java.util.UUID @@ -18,12 +18,9 @@ class NotificationEntity( @Column(length = 36, updatable = false, nullable = false) val id: UUID, - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - val user: UserEntity, - - @Column(nullable = false) - var fcmToken: String, + @JdbcTypeCode(Types.VARCHAR) + @Column(name = "user_id", length = 36, nullable = false) + val userId: UUID, @Column(nullable = false) var title: String, @@ -49,8 +46,7 @@ class NotificationEntity( fun fromDomain(notification: Notification): NotificationEntity { return NotificationEntity( id = notification.id.value, - user = UserEntity.fromDomain(notification.user), - fcmToken = notification.fcmToken, + userId = notification.userId.value, title = notification.title, message = notification.message, notificationType = notification.notificationType, @@ -64,8 +60,7 @@ class NotificationEntity( fun toDomain(): Notification { return Notification.reconstruct( id = Notification.Id.newInstance(this.id), - user = this.user.toDomain(), - fcmToken = this.fcmToken, + userId = User.Id.newInstance(this.userId), title = this.title, message = this.message, notificationType = this.notificationType, diff --git a/infra/src/main/kotlin/org/yapp/infra/notification/repository/JpaNotificationRepository.kt b/infra/src/main/kotlin/org/yapp/infra/notification/repository/JpaNotificationRepository.kt index c1fbf0d1..f8b538c3 100644 --- a/infra/src/main/kotlin/org/yapp/infra/notification/repository/JpaNotificationRepository.kt +++ b/infra/src/main/kotlin/org/yapp/infra/notification/repository/JpaNotificationRepository.kt @@ -6,7 +6,6 @@ import org.springframework.data.jpa.repository.JpaRepository import java.util.UUID interface JpaNotificationRepository : JpaRepository { - fun findByUser(user: UserEntity): NotificationEntity? fun findByUserId(userId: UUID): List fun findByIsSent(isSent: Boolean): List } diff --git a/infra/src/main/kotlin/org/yapp/infra/notification/repository/impl/NotificationRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/notification/repository/impl/NotificationRepositoryImpl.kt index efbc343f..6cac407a 100644 --- a/infra/src/main/kotlin/org/yapp/infra/notification/repository/impl/NotificationRepositoryImpl.kt +++ b/infra/src/main/kotlin/org/yapp/infra/notification/repository/impl/NotificationRepositoryImpl.kt @@ -21,8 +21,7 @@ class NotificationRepositoryImpl( } override fun findByUser(user: User): Notification? { - val userEntity = UserEntity.fromDomain(user) - return jpaNotificationRepository.findByUser(userEntity)?.toDomain() + return jpaNotificationRepository.findByUserId(user.id.value).firstOrNull()?.toDomain() } override fun findByUserId(userId: UUID): List { diff --git a/infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt b/infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt index 66141de3..1b754912 100644 --- a/infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt +++ b/infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt @@ -5,6 +5,7 @@ import org.hibernate.annotations.JdbcTypeCode import org.hibernate.annotations.SQLDelete import org.hibernate.annotations.SQLRestriction import org.yapp.domain.readingrecord.ReadingRecord +import org.yapp.domain.userbook.UserBook import org.yapp.infra.common.BaseTimeEntity import java.sql.Types import java.util.* @@ -45,7 +46,7 @@ class ReadingRecordEntity( fun toDomain(): ReadingRecord { return ReadingRecord.reconstruct( id = ReadingRecord.Id.newInstance(this.id), - userBookId = ReadingRecord.UserBookId.newInstance(this.userBookId), + userBookId = UserBook.Id.newInstance(this.userBookId), pageNumber = ReadingRecord.PageNumber.newInstance(this.pageNumber), quote = ReadingRecord.Quote.newInstance(this.quote), review = ReadingRecord.Review.newInstance(this.review), diff --git a/infra/src/main/kotlin/org/yapp/infra/user/entity/UserEntity.kt b/infra/src/main/kotlin/org/yapp/infra/user/entity/UserEntity.kt index 83f9cd0d..d3868f99 100644 --- a/infra/src/main/kotlin/org/yapp/infra/user/entity/UserEntity.kt +++ b/infra/src/main/kotlin/org/yapp/infra/user/entity/UserEntity.kt @@ -42,9 +42,6 @@ class UserEntity private constructor( appleRefreshToken: String? = null, - @Column(name = "fcm_token", length = 1024) - var fcmToken: String? = null, - @Column(name = "notification_enabled", nullable = false) var notificationEnabled: Boolean = true, @@ -83,7 +80,6 @@ class UserEntity private constructor( role = role, termsAgreed = termsAgreed, appleRefreshToken = appleRefreshToken, - fcmToken = fcmToken, notificationEnabled = notificationEnabled, lastActivity = lastActivity, createdAt = createdAt, @@ -102,7 +98,6 @@ class UserEntity private constructor( role = user.role, termsAgreed = user.termsAgreed, appleRefreshToken = user.appleRefreshToken, - fcmToken = user.fcmToken, notificationEnabled = user.notificationEnabled, lastActivity = user.lastActivity ) diff --git a/infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt b/infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt index 91b6b492..25db5f35 100644 --- a/infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt +++ b/infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt @@ -4,6 +4,8 @@ import jakarta.persistence.* import org.hibernate.annotations.JdbcTypeCode import org.hibernate.annotations.SQLDelete import org.hibernate.annotations.SQLRestriction +import org.yapp.domain.book.Book +import org.yapp.domain.user.User import org.yapp.domain.userbook.BookStatus import org.yapp.domain.userbook.UserBook import org.yapp.infra.common.BaseTimeEntity @@ -71,9 +73,9 @@ class UserBookEntity( fun toDomain(): UserBook = UserBook.reconstruct( id = UserBook.Id.newInstance(this.id), - userId = UserBook.UserId.newInstance(this.userId), - bookId = UserBook.BookId.newInstance(this.bookId), - bookIsbn13 = UserBook.BookIsbn13.newInstance(this.bookIsbn13), + userId = User.Id.newInstance(this.userId), + bookId = Book.Id.newInstance(this.bookId), + bookIsbn13 = Book.Isbn13.newInstance(this.bookIsbn13), coverImageUrl = this.coverImageUrl, publisher = this.publisher, title = this.title, diff --git a/infra/src/main/resources/application-persistence.yml b/infra/src/main/resources/application-persistence.yml index 145871d4..941a12b2 100644 --- a/infra/src/main/resources/application-persistence.yml +++ b/infra/src/main/resources/application-persistence.yml @@ -8,7 +8,7 @@ spring: jpa: database-platform: org.hibernate.dialect.MySQL8Dialect hibernate: - ddl-auto: update + ddl-auto: validate show-sql: true open-in-view: false properties: @@ -16,7 +16,7 @@ spring: format_sql: true flyway: - enabled: false + enabled: true baseline-on-migrate: false validate-on-migrate: true locations: