From e2c70de2e081b6804724c0c4860ab205853c60e4 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 21 Apr 2025 22:03:58 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=ED=96=89=EB=8F=99?= =?UTF-8?q?=20=EA=B8=B0=EB=A1=9D=20=EB=B0=8F=20=ED=8F=AC=EC=9D=B8=ED=8A=B8?= =?UTF-8?q?=20=EC=A7=80=EA=B8=89=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20+=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/member/MemberController.kt | 10 ++- .../sodalive/point/MemberPointRepository.kt | 5 ++ .../sodalive/point/PointGrantLogRepository.kt | 27 ++++++++ .../point/PointRewardPolicyRepository.kt | 34 ++++++++++ .../useraction/UserActionLogRepository.kt | 39 ++++++++++++ .../sodalive/useraction/UserActionService.kt | 62 +++++++++++++++++++ 6 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/point/MemberPointRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/point/PointGrantLogRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/point/PointRewardPolicyRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/useraction/UserActionLogRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/useraction/UserActionService.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt index 0c55544..e26be41 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -13,6 +13,8 @@ import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingReq import kr.co.vividnext.sodalive.member.signUp.SignUpRequestV2 import kr.co.vividnext.sodalive.member.social.google.GoogleAuthService import kr.co.vividnext.sodalive.member.social.kakao.KakaoAuthService +import kr.co.vividnext.sodalive.useraction.ActionType +import kr.co.vividnext.sodalive.useraction.UserActionService import org.springframework.data.domain.Pageable import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.userdetails.User @@ -34,7 +36,8 @@ class MemberController( private val service: MemberService, private val kakaoAuthService: KakaoAuthService, private val googleAuthService: GoogleAuthService, - private val trackingService: AdTrackingService + private val trackingService: AdTrackingService, + private val userActionService: UserActionService ) { @GetMapping("/check/email") fun checkEmail(@RequestParam email: String) = service.duplicateCheckEmail(email) @@ -60,6 +63,11 @@ class MemberController( ) } + userActionService.recordAction( + memberId = response.memberId, + actionType = ActionType.SIGN_UP + ) + return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = response.loginResponse) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/point/MemberPointRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/point/MemberPointRepository.kt new file mode 100644 index 0000000..1fec31b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/point/MemberPointRepository.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.point + +import org.springframework.data.jpa.repository.JpaRepository + +interface MemberPointRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/point/PointGrantLogRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/point/PointGrantLogRepository.kt new file mode 100644 index 0000000..361a873 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/point/PointGrantLogRepository.kt @@ -0,0 +1,27 @@ +package kr.co.vividnext.sodalive.point + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.point.QPointGrantLog.pointGrantLog +import org.springframework.data.jpa.repository.JpaRepository + +interface PointGrantLogRepository : JpaRepository, PointGrantLogQueryRepository + +interface PointGrantLogQueryRepository { + fun existsByMemberIdAndPolicyId(memberId: Long, policyId: Long): Boolean +} + +class PointGrantLogQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : PointGrantLogQueryRepository { + override fun existsByMemberIdAndPolicyId(memberId: Long, policyId: Long): Boolean { + return queryFactory + .select(pointGrantLog.id) + .from(pointGrantLog) + .where( + pointGrantLog.memberId.eq(memberId), + pointGrantLog.policyId.eq(policyId) + ) + .fetch() + .isNotEmpty() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/point/PointRewardPolicyRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/point/PointRewardPolicyRepository.kt new file mode 100644 index 0000000..9587517 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/point/PointRewardPolicyRepository.kt @@ -0,0 +1,34 @@ +package kr.co.vividnext.sodalive.point + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.point.QPointRewardPolicy.pointRewardPolicy +import kr.co.vividnext.sodalive.useraction.ActionType +import org.springframework.data.jpa.repository.JpaRepository +import java.time.LocalDateTime + +interface PointRewardPolicyRepository : JpaRepository, PointRewardPolicyQueryRepository + +interface PointRewardPolicyQueryRepository { + fun findByActionTypeAndIsActiveTrue(actionType: ActionType, nowDateTime: LocalDateTime): PointRewardPolicy? +} + +class PointRewardPolicyQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : PointRewardPolicyQueryRepository { + override fun findByActionTypeAndIsActiveTrue( + actionType: ActionType, + nowDateTime: LocalDateTime + ): PointRewardPolicy? { + return queryFactory + .selectFrom(pointRewardPolicy) + .where( + pointRewardPolicy.isActive, + pointRewardPolicy.actionType.eq(actionType), + pointRewardPolicy.startDate.loe(nowDateTime), + pointRewardPolicy.endDate.goe(nowDateTime) + .or(pointRewardPolicy.endDate.isNull) + ) + .orderBy(pointRewardPolicy.endDate.asc()) + .fetchFirst() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/useraction/UserActionLogRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/useraction/UserActionLogRepository.kt new file mode 100644 index 0000000..0a0f5ec --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/useraction/UserActionLogRepository.kt @@ -0,0 +1,39 @@ +package kr.co.vividnext.sodalive.useraction + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.useraction.QUserActionLog.userActionLog +import org.springframework.data.jpa.repository.JpaRepository +import java.time.LocalDateTime + +interface UserActionLogRepository : JpaRepository, UserActionLogQueryRepository + +interface UserActionLogQueryRepository { + fun countByMemberIdAndActionTypeAndCreatedAtBetween( + memberId: Long, + actionType: ActionType, + startDate: LocalDateTime, + endDate: LocalDateTime + ): Int +} + +class UserActionLogQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : UserActionLogQueryRepository { + override fun countByMemberIdAndActionTypeAndCreatedAtBetween( + memberId: Long, + actionType: ActionType, + startDate: LocalDateTime, + endDate: LocalDateTime + ): Int { + return queryFactory + .select(userActionLog.id) + .from(userActionLog) + .where( + userActionLog.memberId.eq(memberId) + .and(userActionLog.actionType.eq(actionType)) + .and(userActionLog.createdAt.between(startDate, endDate)) + ) + .fetch() + .size + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/useraction/UserActionService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/useraction/UserActionService.kt new file mode 100644 index 0000000..1128897 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/useraction/UserActionService.kt @@ -0,0 +1,62 @@ +package kr.co.vividnext.sodalive.useraction + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kr.co.vividnext.sodalive.point.MemberPoint +import kr.co.vividnext.sodalive.point.MemberPointRepository +import kr.co.vividnext.sodalive.point.PointGrantLog +import kr.co.vividnext.sodalive.point.PointGrantLogRepository +import kr.co.vividnext.sodalive.point.PointRewardPolicyRepository +import org.springframework.stereotype.Service +import java.time.LocalDateTime + +@Service +class UserActionService( + private val repository: UserActionLogRepository, + private val policyRepository: PointRewardPolicyRepository, + private val grantLogRepository: PointGrantLogRepository, + private val memberPointRepository: MemberPointRepository +) { + + private val coroutineScope = CoroutineScope(Dispatchers.IO) + + fun recordAction(memberId: Long, actionType: ActionType) { + coroutineScope.launch { + val now = LocalDateTime.now() + repository.save(UserActionLog(memberId, actionType)) + + val policy = policyRepository.findByActionTypeAndIsActiveTrue(actionType, now) + if (policy != null) { + val actionCount = repository.countByMemberIdAndActionTypeAndCreatedAtBetween( + memberId = memberId, + actionType = actionType, + startDate = policy.startDate, + endDate = policy.endDate ?: now + ) + if (actionCount < policy.threshold) return@launch + + val alreadyGranted = grantLogRepository.existsByMemberIdAndPolicyId(memberId, policy.id!!) + if (alreadyGranted) return@launch + + memberPointRepository.save( + MemberPoint( + memberId = memberId, + point = policy.pointAmount, + actionType = actionType, + expiresAt = now.plusDays(3) + ) + ) + + grantLogRepository.save( + PointGrantLog( + memberId = memberId, + point = policy.pointAmount, + actionType = actionType, + policyId = policy.id!! + ) + ) + } + } + } +}