| @@ -2,14 +2,17 @@ package kr.co.vividnext.sodalive.admin.point | |||||||
|  |  | ||||||
| import kr.co.vividnext.sodalive.point.PointRewardPolicy | import kr.co.vividnext.sodalive.point.PointRewardPolicy | ||||||
| import kr.co.vividnext.sodalive.useraction.ActionType | import kr.co.vividnext.sodalive.useraction.ActionType | ||||||
|  | import kr.co.vividnext.sodalive.useraction.PolicyType | ||||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||||
| import java.time.ZoneId | import java.time.ZoneId | ||||||
| import java.time.format.DateTimeFormatter | import java.time.format.DateTimeFormatter | ||||||
|  |  | ||||||
| data class CreatePointRewardPolicyRequest( | data class CreatePointRewardPolicyRequest( | ||||||
|     val title: String, |     val title: String, | ||||||
|  |     val policyType: PolicyType, | ||||||
|     val actionType: ActionType, |     val actionType: ActionType, | ||||||
|     val threshold: Int, |     val threshold: Int, | ||||||
|  |     val availableCount: Int, | ||||||
|     val pointAmount: Int, |     val pointAmount: Int, | ||||||
|     val startDate: String, |     val startDate: String, | ||||||
|     val endDate: String |     val endDate: String | ||||||
| @@ -19,8 +22,10 @@ data class CreatePointRewardPolicyRequest( | |||||||
|  |  | ||||||
|         return PointRewardPolicy( |         return PointRewardPolicy( | ||||||
|             title = title, |             title = title, | ||||||
|  |             policyType = policyType, | ||||||
|             actionType = actionType, |             actionType = actionType, | ||||||
|             threshold = threshold, |             threshold = threshold, | ||||||
|  |             availableCount = availableCount, | ||||||
|             pointAmount = pointAmount, |             pointAmount = pointAmount, | ||||||
|             startDate = LocalDateTime.parse(startDate, dateTimeFormatter) |             startDate = LocalDateTime.parse(startDate, dateTimeFormatter) | ||||||
|                 .atZone(ZoneId.of("Asia/Seoul")) |                 .atZone(ZoneId.of("Asia/Seoul")) | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.admin.point | |||||||
|  |  | ||||||
| import com.querydsl.core.annotations.QueryProjection | import com.querydsl.core.annotations.QueryProjection | ||||||
| import kr.co.vividnext.sodalive.useraction.ActionType | import kr.co.vividnext.sodalive.useraction.ActionType | ||||||
|  | import kr.co.vividnext.sodalive.useraction.PolicyType | ||||||
|  |  | ||||||
| data class GetPointRewardPolicyListResponse( | data class GetPointRewardPolicyListResponse( | ||||||
|     val totalCount: Int, |     val totalCount: Int, | ||||||
| @@ -11,8 +12,10 @@ data class GetPointRewardPolicyListResponse( | |||||||
| data class GetPointRewardPolicyListItem @QueryProjection constructor( | data class GetPointRewardPolicyListItem @QueryProjection constructor( | ||||||
|     val id: Long, |     val id: Long, | ||||||
|     val title: String, |     val title: String, | ||||||
|  |     val policyType: PolicyType, | ||||||
|     val actionType: ActionType, |     val actionType: ActionType, | ||||||
|     val threshold: Int, |     val threshold: Int, | ||||||
|  |     val availableCount: Int, | ||||||
|     val pointAmount: Int, |     val pointAmount: Int, | ||||||
|     val startDate: String, |     val startDate: String, | ||||||
|     val endDate: String, |     val endDate: String, | ||||||
|   | |||||||
| @@ -33,8 +33,10 @@ class PointPolicyQueryRepositoryImpl( | |||||||
|                 QGetPointRewardPolicyListItem( |                 QGetPointRewardPolicyListItem( | ||||||
|                     pointRewardPolicy.id, |                     pointRewardPolicy.id, | ||||||
|                     pointRewardPolicy.title, |                     pointRewardPolicy.title, | ||||||
|  |                     pointRewardPolicy.policyType, | ||||||
|                     pointRewardPolicy.actionType, |                     pointRewardPolicy.actionType, | ||||||
|                     pointRewardPolicy.threshold, |                     pointRewardPolicy.threshold, | ||||||
|  |                     pointRewardPolicy.availableCount, | ||||||
|                     pointRewardPolicy.pointAmount, |                     pointRewardPolicy.pointAmount, | ||||||
|                     getFormattedDate(pointRewardPolicy.startDate), |                     getFormattedDate(pointRewardPolicy.startDate), | ||||||
|                     getFormattedDate(pointRewardPolicy.endDate), |                     getFormattedDate(pointRewardPolicy.endDate), | ||||||
|   | |||||||
| @@ -3,6 +3,9 @@ package kr.co.vividnext.sodalive.content.comment | |||||||
| import kr.co.vividnext.sodalive.common.ApiResponse | import kr.co.vividnext.sodalive.common.ApiResponse | ||||||
| import kr.co.vividnext.sodalive.common.SodaException | import kr.co.vividnext.sodalive.common.SodaException | ||||||
| import kr.co.vividnext.sodalive.member.Member | import kr.co.vividnext.sodalive.member.Member | ||||||
|  | import kr.co.vividnext.sodalive.member.MemberService | ||||||
|  | import kr.co.vividnext.sodalive.useraction.ActionType | ||||||
|  | import kr.co.vividnext.sodalive.useraction.UserActionService | ||||||
| import org.springframework.data.domain.Pageable | import org.springframework.data.domain.Pageable | ||||||
| import org.springframework.security.core.annotation.AuthenticationPrincipal | import org.springframework.security.core.annotation.AuthenticationPrincipal | ||||||
| import org.springframework.web.bind.annotation.GetMapping | import org.springframework.web.bind.annotation.GetMapping | ||||||
| @@ -14,7 +17,11 @@ import org.springframework.web.bind.annotation.RequestParam | |||||||
| import org.springframework.web.bind.annotation.RestController | import org.springframework.web.bind.annotation.RestController | ||||||
|  |  | ||||||
| @RestController | @RestController | ||||||
| class AudioContentCommentController(private val service: AudioContentCommentService) { | class AudioContentCommentController( | ||||||
|  |     private val service: AudioContentCommentService, | ||||||
|  |     private val memberService: MemberService, | ||||||
|  |     private val userActionService: UserActionService | ||||||
|  | ) { | ||||||
|     @PostMapping("/audio-content/comment") |     @PostMapping("/audio-content/comment") | ||||||
|     fun registerComment( |     fun registerComment( | ||||||
|         @RequestBody request: RegisterCommentRequest, |         @RequestBody request: RegisterCommentRequest, | ||||||
| @@ -22,15 +29,38 @@ class AudioContentCommentController(private val service: AudioContentCommentServ | |||||||
|     ) = run { |     ) = run { | ||||||
|         if (member == null) throw SodaException("로그인 정보를 확인해주세요.") |         if (member == null) throw SodaException("로그인 정보를 확인해주세요.") | ||||||
|  |  | ||||||
|         ApiResponse.ok( |         val commentId = service.registerComment( | ||||||
|             service.registerComment( |  | ||||||
|             comment = request.comment, |             comment = request.comment, | ||||||
|             audioContentId = request.contentId, |             audioContentId = request.contentId, | ||||||
|             parentId = request.parentId, |             parentId = request.parentId, | ||||||
|             isSecret = request.isSecret, |             isSecret = request.isSecret, | ||||||
|             member = member |             member = member | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             val memberId = member.id!! | ||||||
|  |             val pushTokenList = memberService.getPushTokenList(recipient = memberId) | ||||||
|  |  | ||||||
|  |             userActionService.recordAction( | ||||||
|  |                 memberId = member.id!!, | ||||||
|  |                 isAuth = member.auth != null, | ||||||
|  |                 actionType = ActionType.CONTENT_COMMENT, | ||||||
|  |                 contentCommentId = commentId, | ||||||
|  |                 pushTokenList = pushTokenList | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  |             userActionService.recordAction( | ||||||
|  |                 memberId = member.id!!, | ||||||
|  |                 isAuth = member.auth != null, | ||||||
|  |                 actionType = ActionType.ORDER_CONTENT_COMMENT, | ||||||
|  |                 contentId = request.contentId, | ||||||
|  |                 contentCommentId = commentId, | ||||||
|  |                 pushTokenList = pushTokenList | ||||||
|  |             ) | ||||||
|  |         } catch (_: Exception) { | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ApiResponse.ok(Unit, "") | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @PutMapping("/audio-content/comment") |     @PutMapping("/audio-content/comment") | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ class AudioContentCommentService( | |||||||
|         audioContentId: Long, |         audioContentId: Long, | ||||||
|         parentId: Long? = null, |         parentId: Long? = null, | ||||||
|         isSecret: Boolean = false |         isSecret: Boolean = false | ||||||
|     ) { |     ): Long { | ||||||
|         val audioContent = audioContentRepository.findByIdOrNull(id = audioContentId) |         val audioContent = audioContentRepository.findByIdOrNull(id = audioContentId) | ||||||
|             ?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") |             ?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") | ||||||
|  |  | ||||||
| @@ -64,7 +64,7 @@ class AudioContentCommentService( | |||||||
|             audioContentComment.parent = parent |             audioContentComment.parent = parent | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         repository.save(audioContentComment) |         val savedContentComment = repository.save(audioContentComment) | ||||||
|  |  | ||||||
|         applicationEventPublisher.publishEvent( |         applicationEventPublisher.publishEvent( | ||||||
|             FcmEvent( |             FcmEvent( | ||||||
| @@ -84,6 +84,8 @@ class AudioContentCommentService( | |||||||
|                 myMemberId = member.id |                 myMemberId = member.id | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |         return savedContentComment.id!! | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Transactional |     @Transactional | ||||||
|   | |||||||
| @@ -44,6 +44,7 @@ interface OrderQueryRepository { | |||||||
|     fun findOrderedContent(contentIdList: List<Long>, memberId: Long): List<Long> |     fun findOrderedContent(contentIdList: List<Long>, memberId: Long): List<Long> | ||||||
|     fun findEndDateByContentId(contentIdList: List<Long>, memberId: Long): List<ContentIdAndEndDateData> |     fun findEndDateByContentId(contentIdList: List<Long>, memberId: Long): List<ContentIdAndEndDateData> | ||||||
|     fun findBuyerListByContentId(contentId: Long): List<ContentBuyer> |     fun findBuyerListByContentId(contentId: Long): List<ContentBuyer> | ||||||
|  |     fun findByMemberIdAndContentId(memberId: Long, contentId: Long, createdAt: LocalDateTime): Order? | ||||||
| } | } | ||||||
|  |  | ||||||
| @Repository | @Repository | ||||||
| @@ -280,4 +281,17 @@ class OrderQueryRepositoryImpl( | |||||||
|             ) |             ) | ||||||
|             .fetch() |             .fetch() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     override fun findByMemberIdAndContentId(memberId: Long, contentId: Long, createdAt: LocalDateTime): Order? { | ||||||
|  |         return queryFactory | ||||||
|  |             .selectFrom(order) | ||||||
|  |             .where( | ||||||
|  |                 order.isActive.isTrue, | ||||||
|  |                 order.member.id.eq(memberId), | ||||||
|  |                 order.audioContent.id.eq(contentId), | ||||||
|  |                 order.createdAt.after(createdAt) | ||||||
|  |             ) | ||||||
|  |             .orderBy(order.id.desc()) | ||||||
|  |             .fetchFirst() | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ class OrderService( | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         val usedPoint = if (order.type == OrderType.RENTAL && content.isPointAvailable) { |         val usedPoint = if (order.type == OrderType.RENTAL && content.isPointAvailable) { | ||||||
|             pointUsageService.usePoint(member.id!!, order.can) |             pointUsageService.usePoint(member.id!!, order.can, orderId = order.id) | ||||||
|         } else { |         } else { | ||||||
|             0 |             0 | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -4,8 +4,6 @@ import com.google.firebase.messaging.AndroidConfig | |||||||
| import com.google.firebase.messaging.ApnsConfig | import com.google.firebase.messaging.ApnsConfig | ||||||
| import com.google.firebase.messaging.Aps | import com.google.firebase.messaging.Aps | ||||||
| import com.google.firebase.messaging.FirebaseMessaging | import com.google.firebase.messaging.FirebaseMessaging | ||||||
| import com.google.firebase.messaging.FirebaseMessagingException |  | ||||||
| import com.google.firebase.messaging.Message |  | ||||||
| import com.google.firebase.messaging.MessagingErrorCode | import com.google.firebase.messaging.MessagingErrorCode | ||||||
| import com.google.firebase.messaging.MulticastMessage | import com.google.firebase.messaging.MulticastMessage | ||||||
| import com.google.firebase.messaging.Notification | import com.google.firebase.messaging.Notification | ||||||
| @@ -38,7 +36,7 @@ class FcmService(private val pushTokenService: PushTokenService) { | |||||||
|  |  | ||||||
|         while (attempt <= maxAttempts && targets.isNotEmpty()) { |         while (attempt <= maxAttempts && targets.isNotEmpty()) { | ||||||
|             val multicastMessage = MulticastMessage.builder() |             val multicastMessage = MulticastMessage.builder() | ||||||
|                 .addAllTokens(tokens) |                 .addAllTokens(targets) | ||||||
|  |  | ||||||
|             multicastMessage.setAndroidConfig( |             multicastMessage.setAndroidConfig( | ||||||
|                 AndroidConfig.builder() |                 AndroidConfig.builder() | ||||||
| @@ -117,51 +115,69 @@ class FcmService(private val pushTokenService: PushTokenService) { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun sendPointGranted(token: String, point: Int) { |     fun sendPointGranted(tokens: List<String>, point: Int) { | ||||||
|  |         if (tokens.isEmpty()) return | ||||||
|         val data = mapOf( |         val data = mapOf( | ||||||
|             "type" to "POINT_GRANTED", |             "type" to "POINT_GRANTED", | ||||||
|             "point" to point.toString(), |             "point" to point.toString(), | ||||||
|             "message" to "${point}포인트가 지급되었습니다!" |             "message" to "${point}포인트가 지급되었습니다!" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |         var targets = tokens | ||||||
|         var attempts = 0 |         var attempts = 0 | ||||||
|         val maxAttempts = 3 |         val maxAttempts = 3 | ||||||
|  |  | ||||||
|         while (attempts < maxAttempts) { |         while (attempts <= maxAttempts && targets.isNotEmpty()) { | ||||||
|             try { |             val multicastMessage = MulticastMessage.builder() | ||||||
|                 val message = Message.builder() |                 .addAllTokens(targets) | ||||||
|                     .setToken(token) |  | ||||||
|                 .putAllData(data) |                 .putAllData(data) | ||||||
|  |  | ||||||
|  |             multicastMessage.setAndroidConfig( | ||||||
|  |                 AndroidConfig.builder() | ||||||
|  |                     .setPriority(AndroidConfig.Priority.HIGH) | ||||||
|                     .build() |                     .build() | ||||||
|  |             ) | ||||||
|  |  | ||||||
|                 val response = FirebaseMessaging.getInstance().send(message) |             multicastMessage.setApnsConfig( | ||||||
|                 logger.info("[FCM] ✅ 성공 (attempt ${attempts + 1}): messageId=$response") |                 ApnsConfig.builder() | ||||||
|                 return // 성공 시 즉시 종료 |                     .setAps( | ||||||
|             } catch (e: FirebaseMessagingException) { |                         Aps.builder() | ||||||
|                 attempts++ |                             .setSound("default") | ||||||
|  |                             .build() | ||||||
|  |                     ) | ||||||
|  |                     .build() | ||||||
|  |             ) | ||||||
|  |  | ||||||
|                 // "registration-token-not-registered" 예외 코드 확인 |             val response = FirebaseMessaging.getInstance().sendEachForMulticast(multicastMessage.build()) | ||||||
|                 if (e.messagingErrorCode == MessagingErrorCode.UNREGISTERED) { |             val failedTokens = mutableListOf<String>() | ||||||
|                     logger.error("[FCM] ❌ 실패: 토큰이 등록되지 않음 (등록 해제됨) → 재시도 안함") |  | ||||||
|  |             response.responses.forEachIndexed { index, res -> | ||||||
|  |                 if (!res.isSuccessful) { | ||||||
|  |                     val exception = res.exception | ||||||
|  |                     val token = targets[index] | ||||||
|  |  | ||||||
|  |                     if (exception?.messagingErrorCode == MessagingErrorCode.UNREGISTERED) { | ||||||
|  |                         logger.error("[FCM] ❌ UNREGISTERED → $token") | ||||||
|                         // DB에서 삭제 |                         // DB에서 삭제 | ||||||
|                         pushTokenService.unregisterInvalidToken(token) |                         pushTokenService.unregisterInvalidToken(token) | ||||||
|  |                     } else { | ||||||
|  |                         logger.error("[FCM] ❌ 실패: $token / ${exception?.messagingErrorCode}") | ||||||
|  |                         failedTokens.add(token) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (failedTokens.isEmpty()) { | ||||||
|  |                 logger.info("[FCM] ✅ 전체 전송 성공") | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|  |  | ||||||
|                 logger.error("[FCM] ❌ 실패 (attempt $attempts): ${e.errorCode} - ${e.message}") |             targets = failedTokens | ||||||
|  |  | ||||||
|                 if (attempts >= maxAttempts) { |  | ||||||
|                     logger.error("[FCM] ❌ 최종 실패: 전송 불가") |  | ||||||
|                 } |  | ||||||
|             } catch (e: Exception) { |  | ||||||
|                 // Firebase 이외의 예외도 잡기 |  | ||||||
|             attempts++ |             attempts++ | ||||||
|                 logger.error("[FCM] ❌ 실패 (attempt $attempts): ${e.message}") |         } | ||||||
|  |  | ||||||
|                 if (attempts >= maxAttempts) { |         if (targets.isNotEmpty()) { | ||||||
|                     logger.error("[FCM] ❌ 최종 실패: 알 수 없는 오류") |             logger.error("[FCM] ❌ 최종 실패 대상 ${targets.size}명 → $targets") | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -65,8 +65,13 @@ class MemberController( | |||||||
|  |  | ||||||
|         userActionService.recordAction( |         userActionService.recordAction( | ||||||
|             memberId = response.memberId, |             memberId = response.memberId, | ||||||
|  |             isAuth = false, | ||||||
|             actionType = ActionType.SIGN_UP, |             actionType = ActionType.SIGN_UP, | ||||||
|             pushToken = request.pushToken |             pushTokenList = if (request.pushToken != null) { | ||||||
|  |                 listOf(request.pushToken) | ||||||
|  |             } else { | ||||||
|  |                 emptyList() | ||||||
|  |             } | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = response.loginResponse) |         return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = response.loginResponse) | ||||||
| @@ -354,8 +359,13 @@ class MemberController( | |||||||
|         if (response.isNew) { |         if (response.isNew) { | ||||||
|             userActionService.recordAction( |             userActionService.recordAction( | ||||||
|                 memberId = response.memberId, |                 memberId = response.memberId, | ||||||
|  |                 isAuth = false, | ||||||
|                 actionType = ActionType.SIGN_UP, |                 actionType = ActionType.SIGN_UP, | ||||||
|                 pushToken = request.pushToken |                 pushTokenList = if (request.pushToken != null) { | ||||||
|  |                     listOf(request.pushToken) | ||||||
|  |                 } else { | ||||||
|  |                     emptyList() | ||||||
|  |                 } | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -385,8 +395,13 @@ class MemberController( | |||||||
|         if (response.isNew) { |         if (response.isNew) { | ||||||
|             userActionService.recordAction( |             userActionService.recordAction( | ||||||
|                 memberId = response.memberId, |                 memberId = response.memberId, | ||||||
|  |                 isAuth = false, | ||||||
|                 actionType = ActionType.SIGN_UP, |                 actionType = ActionType.SIGN_UP, | ||||||
|                 pushToken = request.pushToken |                 pushTokenList = if (request.pushToken != null) { | ||||||
|  |                     listOf(request.pushToken) | ||||||
|  |                 } else { | ||||||
|  |                     emptyList() | ||||||
|  |                 } | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -64,6 +64,8 @@ interface MemberQueryRepository { | |||||||
|  |  | ||||||
|     fun existsByNickname(nickname: String): Boolean |     fun existsByNickname(nickname: String): Boolean | ||||||
|     fun findNicknamesWithPrefix(prefix: String): List<String> |     fun findNicknamesWithPrefix(prefix: String): List<String> | ||||||
|  |  | ||||||
|  |     fun getPushTokenList(memberId: Long): List<String> | ||||||
| } | } | ||||||
|  |  | ||||||
| @Repository | @Repository | ||||||
| @@ -507,4 +509,17 @@ class MemberQueryRepositoryImpl( | |||||||
|             ) |             ) | ||||||
|             .fetch() |             .fetch() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     override fun getPushTokenList(memberId: Long): List<String> { | ||||||
|  |         val where = member.isActive.isTrue | ||||||
|  |             .and(member.email.notIn("admin@sodalive.net")) | ||||||
|  |             .and(member.id.eq(memberId)) | ||||||
|  |  | ||||||
|  |         return queryFactory | ||||||
|  |             .select(pushToken.token) | ||||||
|  |             .from(member) | ||||||
|  |             .innerJoin(pushToken).on(member.id.eq(pushToken.member.id)) | ||||||
|  |             .where(where) | ||||||
|  |             .fetch() | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -907,6 +907,12 @@ class MemberService( | |||||||
|         return MemberResolveResult(member = member, isNew = true) |         return MemberResolveResult(member = member, isNew = true) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     fun getPushTokenList(recipient: Long): List<String> { | ||||||
|  |         return repository.getPushTokenList(recipient) | ||||||
|  |             .toSet() | ||||||
|  |             .toList() | ||||||
|  |     } | ||||||
|  |  | ||||||
|     private fun checkEmail(email: String) { |     private fun checkEmail(email: String) { | ||||||
|         val member = repository.findByEmail(email) |         val member = repository.findByEmail(email) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.member.auth | |||||||
| import kr.co.vividnext.sodalive.common.ApiResponse | import kr.co.vividnext.sodalive.common.ApiResponse | ||||||
| import kr.co.vividnext.sodalive.common.SodaException | import kr.co.vividnext.sodalive.common.SodaException | ||||||
| import kr.co.vividnext.sodalive.member.Member | import kr.co.vividnext.sodalive.member.Member | ||||||
|  | import kr.co.vividnext.sodalive.member.MemberService | ||||||
| import kr.co.vividnext.sodalive.useraction.ActionType | import kr.co.vividnext.sodalive.useraction.ActionType | ||||||
| import kr.co.vividnext.sodalive.useraction.UserActionService | import kr.co.vividnext.sodalive.useraction.UserActionService | ||||||
| import org.springframework.security.core.annotation.AuthenticationPrincipal | import org.springframework.security.core.annotation.AuthenticationPrincipal | ||||||
| @@ -15,6 +16,7 @@ import org.springframework.web.bind.annotation.RestController | |||||||
| @RequestMapping("/auth") | @RequestMapping("/auth") | ||||||
| class AuthController( | class AuthController( | ||||||
|     private val service: AuthService, |     private val service: AuthService, | ||||||
|  |     private val memberService: MemberService, | ||||||
|     private val userActionService: UserActionService |     private val userActionService: UserActionService | ||||||
| ) { | ) { | ||||||
|     @PostMapping |     @PostMapping | ||||||
| @@ -33,11 +35,17 @@ class AuthController( | |||||||
|  |  | ||||||
|         val authResponse = service.authenticate(authenticateData, member.id!!) |         val authResponse = service.authenticate(authenticateData, member.id!!) | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             val memberId = member.id!! | ||||||
|  |             val pushTokenList = memberService.getPushTokenList(recipient = memberId) | ||||||
|             userActionService.recordAction( |             userActionService.recordAction( | ||||||
|                 memberId = member.id!!, |                 memberId = member.id!!, | ||||||
|  |                 isAuth = true, | ||||||
|                 actionType = ActionType.USER_AUTHENTICATION, |                 actionType = ActionType.USER_AUTHENTICATION, | ||||||
|             pushToken = member.pushToken |                 pushTokenList = pushTokenList | ||||||
|             ) |             ) | ||||||
|  |         } catch (_: Exception) { | ||||||
|  |         } | ||||||
|  |  | ||||||
|         ApiResponse.ok(authResponse) |         ApiResponse.ok(authResponse) | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | package kr.co.vividnext.sodalive.point | ||||||
|  |  | ||||||
|  | import com.querydsl.core.annotations.QueryProjection | ||||||
|  |  | ||||||
|  | data class GetPointRewardStatusResponse @QueryProjection constructor( | ||||||
|  |     val rewardPoint: String, | ||||||
|  |     val date: String, | ||||||
|  |     val method: String | ||||||
|  | ) | ||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | package kr.co.vividnext.sodalive.point | ||||||
|  |  | ||||||
|  | data class GetPointStatusResponse(val point: Int) | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | package kr.co.vividnext.sodalive.point | ||||||
|  |  | ||||||
|  | import com.querydsl.core.annotations.QueryProjection | ||||||
|  |  | ||||||
|  | data class GetPointUseStatusResponse @QueryProjection constructor( | ||||||
|  |     val title: String, | ||||||
|  |     val date: String, | ||||||
|  |     val point: Int | ||||||
|  | ) | ||||||
| @@ -0,0 +1,49 @@ | |||||||
|  | package kr.co.vividnext.sodalive.point | ||||||
|  |  | ||||||
|  | import kr.co.vividnext.sodalive.common.ApiResponse | ||||||
|  | import kr.co.vividnext.sodalive.common.SodaException | ||||||
|  | import kr.co.vividnext.sodalive.member.Member | ||||||
|  | import org.springframework.security.core.annotation.AuthenticationPrincipal | ||||||
|  | import org.springframework.web.bind.annotation.GetMapping | ||||||
|  | import org.springframework.web.bind.annotation.RequestMapping | ||||||
|  | import org.springframework.web.bind.annotation.RequestParam | ||||||
|  | import org.springframework.web.bind.annotation.RestController | ||||||
|  |  | ||||||
|  | @RestController | ||||||
|  | @RequestMapping("/point") | ||||||
|  | class PointController(private val service: PointService) { | ||||||
|  |     @GetMapping("/status") | ||||||
|  |     fun getPointStatus( | ||||||
|  |         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? | ||||||
|  |     ) = run { | ||||||
|  |         if (member == null) { | ||||||
|  |             throw SodaException("로그인 정보를 확인해주세요.") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ApiResponse.ok(service.getPointStatus(member)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @GetMapping("/status/use") | ||||||
|  |     fun getPointUseStatus( | ||||||
|  |         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, | ||||||
|  |         @RequestParam("timezone") timezone: String | ||||||
|  |     ) = run { | ||||||
|  |         if (member == null) { | ||||||
|  |             throw SodaException("로그인 정보를 확인해주세요.") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ApiResponse.ok(service.getPointUseStatus(member, timezone)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @GetMapping("/status/reward") | ||||||
|  |     fun getPointRewardStatus( | ||||||
|  |         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, | ||||||
|  |         @RequestParam("timezone") timezone: String | ||||||
|  |     ) = run { | ||||||
|  |         if (member == null) { | ||||||
|  |             throw SodaException("로그인 정보를 확인해주세요.") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ApiResponse.ok(service.getPointRewardStatus(member, timezone)) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -12,5 +12,6 @@ data class PointGrantLog( | |||||||
|     val point: Int, |     val point: Int, | ||||||
|     @Enumerated(EnumType.STRING) |     @Enumerated(EnumType.STRING) | ||||||
|     val actionType: ActionType, |     val actionType: ActionType, | ||||||
|     val policyId: Long? |     val policyId: Long?, | ||||||
|  |     val orderId: Long? | ||||||
| ) : BaseEntity() | ) : BaseEntity() | ||||||
|   | |||||||
| @@ -1,27 +1,75 @@ | |||||||
| package kr.co.vividnext.sodalive.point | package kr.co.vividnext.sodalive.point | ||||||
|  |  | ||||||
|  | import com.querydsl.core.types.dsl.Expressions | ||||||
| import com.querydsl.jpa.impl.JPAQueryFactory | import com.querydsl.jpa.impl.JPAQueryFactory | ||||||
| import kr.co.vividnext.sodalive.point.QPointGrantLog.pointGrantLog | import kr.co.vividnext.sodalive.point.QPointGrantLog.pointGrantLog | ||||||
|  | import kr.co.vividnext.sodalive.point.QPointRewardPolicy.pointRewardPolicy | ||||||
| import org.springframework.data.jpa.repository.JpaRepository | import org.springframework.data.jpa.repository.JpaRepository | ||||||
|  | import java.time.LocalDateTime | ||||||
|  |  | ||||||
| interface PointGrantLogRepository : JpaRepository<PointGrantLog, Long>, PointGrantLogQueryRepository | interface PointGrantLogRepository : JpaRepository<PointGrantLog, Long>, PointGrantLogQueryRepository | ||||||
|  |  | ||||||
| interface PointGrantLogQueryRepository { | interface PointGrantLogQueryRepository { | ||||||
|     fun existsByMemberIdAndPolicyId(memberId: Long, policyId: Long): Boolean |     fun countByMemberIdAndPolicyIdAndStartDate( | ||||||
|  |         memberId: Long, | ||||||
|  |         policyId: Long, | ||||||
|  |         startDate: LocalDateTime, | ||||||
|  |         orderId: Long? = null | ||||||
|  |     ): Int | ||||||
|  |  | ||||||
|  |     fun getPointRewardStatusByMemberId(memberId: Long, timezone: String): List<GetPointRewardStatusResponse> | ||||||
| } | } | ||||||
|  |  | ||||||
| class PointGrantLogQueryRepositoryImpl( | class PointGrantLogQueryRepositoryImpl( | ||||||
|     private val queryFactory: JPAQueryFactory |     private val queryFactory: JPAQueryFactory | ||||||
| ) : PointGrantLogQueryRepository { | ) : PointGrantLogQueryRepository { | ||||||
|     override fun existsByMemberIdAndPolicyId(memberId: Long, policyId: Long): Boolean { |     override fun countByMemberIdAndPolicyIdAndStartDate( | ||||||
|  |         memberId: Long, | ||||||
|  |         policyId: Long, | ||||||
|  |         startDate: LocalDateTime, | ||||||
|  |         orderId: Long? | ||||||
|  |     ): Int { | ||||||
|  |         var where = pointGrantLog.memberId.eq(memberId) | ||||||
|  |             .and(pointGrantLog.policyId.eq(policyId)) | ||||||
|  |             .and(pointGrantLog.createdAt.goe(startDate)) | ||||||
|  |  | ||||||
|  |         if (orderId != null) { | ||||||
|  |             where = where.and(pointGrantLog.orderId.eq(orderId)) | ||||||
|  |         } | ||||||
|  |  | ||||||
|         return queryFactory |         return queryFactory | ||||||
|             .select(pointGrantLog.id) |             .select(pointGrantLog.id) | ||||||
|             .from(pointGrantLog) |             .from(pointGrantLog) | ||||||
|             .where( |             .where(where) | ||||||
|                 pointGrantLog.memberId.eq(memberId), |             .fetch() | ||||||
|                 pointGrantLog.policyId.eq(policyId) |             .size | ||||||
|             ) |     } | ||||||
|  |  | ||||||
|  |     override fun getPointRewardStatusByMemberId(memberId: Long, timezone: String): List<GetPointRewardStatusResponse> { | ||||||
|  |         val formattedDate = Expressions.stringTemplate( | ||||||
|  |             "DATE_FORMAT({0}, {1})", | ||||||
|  |             Expressions.dateTimeTemplate( | ||||||
|  |                 LocalDateTime::class.java, | ||||||
|  |                 "CONVERT_TZ({0},{1},{2})", | ||||||
|  |                 pointGrantLog.createdAt, | ||||||
|  |                 "UTC", | ||||||
|  |                 timezone | ||||||
|  |             ), | ||||||
|  |             "%Y.%m.%d | %H:%i:%s" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return queryFactory | ||||||
|  |             .select( | ||||||
|  |                 QGetPointRewardStatusResponse( | ||||||
|  |                     pointGrantLog.point.stringValue().concat(" 포인트"), | ||||||
|  |                     formattedDate, | ||||||
|  |                     pointRewardPolicy.title | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             .from(pointGrantLog) | ||||||
|  |             .innerJoin(pointRewardPolicy).on(pointGrantLog.policyId.eq(pointRewardPolicy.id)) | ||||||
|  |             .where(pointGrantLog.memberId.eq(memberId)) | ||||||
|  |             .orderBy(pointGrantLog.id.desc()) | ||||||
|             .fetch() |             .fetch() | ||||||
|             .isNotEmpty() |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.point | |||||||
|  |  | ||||||
| import kr.co.vividnext.sodalive.common.BaseEntity | import kr.co.vividnext.sodalive.common.BaseEntity | ||||||
| import kr.co.vividnext.sodalive.useraction.ActionType | import kr.co.vividnext.sodalive.useraction.ActionType | ||||||
|  | import kr.co.vividnext.sodalive.useraction.PolicyType | ||||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||||
| import javax.persistence.Entity | import javax.persistence.Entity | ||||||
| import javax.persistence.EnumType | import javax.persistence.EnumType | ||||||
| @@ -11,8 +12,11 @@ import javax.persistence.Enumerated | |||||||
| data class PointRewardPolicy( | data class PointRewardPolicy( | ||||||
|     var title: String, |     var title: String, | ||||||
|     @Enumerated(EnumType.STRING) |     @Enumerated(EnumType.STRING) | ||||||
|  |     val policyType: PolicyType, | ||||||
|  |     @Enumerated(EnumType.STRING) | ||||||
|     val actionType: ActionType, |     val actionType: ActionType, | ||||||
|     val threshold: Int, |     val threshold: Int, | ||||||
|  |     val availableCount: Int, | ||||||
|     val pointAmount: Int, |     val pointAmount: Int, | ||||||
|     var startDate: LocalDateTime, |     var startDate: LocalDateTime, | ||||||
|     var endDate: LocalDateTime? = null, |     var endDate: LocalDateTime? = null, | ||||||
|   | |||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | package kr.co.vividnext.sodalive.point | ||||||
|  |  | ||||||
|  | import kr.co.vividnext.sodalive.member.Member | ||||||
|  | import org.springframework.stereotype.Service | ||||||
|  | import java.time.LocalDateTime | ||||||
|  |  | ||||||
|  | @Service | ||||||
|  | class PointService( | ||||||
|  |     private val pointGrantLogRepository: PointGrantLogRepository, | ||||||
|  |     private val memberPointRepository: MemberPointRepository, | ||||||
|  |     private val usePointRepository: UsePointRepository | ||||||
|  | ) { | ||||||
|  |     fun getPointStatus(member: Member): GetPointStatusResponse { | ||||||
|  |         return GetPointStatusResponse( | ||||||
|  |             point = memberPointRepository.findByMemberIdAndExpiresAtAfterOrderByExpiresAtAsc( | ||||||
|  |                 memberId = member.id!!, | ||||||
|  |                 expiresAt = LocalDateTime.now() | ||||||
|  |             ).sumOf { it.point } | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun getPointUseStatus(member: Member, timezone: String): List<GetPointUseStatusResponse> { | ||||||
|  |         return usePointRepository.getPointUseStatusByMemberId(member.id!!, timezone) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun getPointRewardStatus(member: Member, timezone: String): List<GetPointRewardStatusResponse> { | ||||||
|  |         return pointGrantLogRepository.getPointRewardStatusByMemberId(member.id!!, timezone) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -8,7 +8,7 @@ class PointUsageService( | |||||||
|     private val memberPointRepository: MemberPointRepository, |     private val memberPointRepository: MemberPointRepository, | ||||||
|     private val usePointRepository: UsePointRepository |     private val usePointRepository: UsePointRepository | ||||||
| ) { | ) { | ||||||
|     fun usePoint(memberId: Long, contentPrice: Int): Int { |     fun usePoint(memberId: Long, contentPrice: Int, orderId: Long?): Int { | ||||||
|         val now = LocalDateTime.now() |         val now = LocalDateTime.now() | ||||||
|         val maxUsablePoint = contentPrice * 10 |         val maxUsablePoint = contentPrice * 10 | ||||||
|  |  | ||||||
| @@ -33,7 +33,7 @@ class PointUsageService( | |||||||
|  |  | ||||||
|         if (used > 0) { |         if (used > 0) { | ||||||
|             memberPointRepository.saveAll(points) |             memberPointRepository.saveAll(points) | ||||||
|             usePointRepository.save(UsePoint(memberId = memberId, amount = used)) |             usePointRepository.save(UsePoint(memberId = memberId, amount = used, orderId = orderId)) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return used |         return used | ||||||
|   | |||||||
| @@ -6,5 +6,6 @@ import javax.persistence.Entity | |||||||
| @Entity | @Entity | ||||||
| data class UsePoint( | data class UsePoint( | ||||||
|     val memberId: Long, |     val memberId: Long, | ||||||
|     val amount: Int |     val amount: Int, | ||||||
|  |     val orderId: Long? = null | ||||||
| ) : BaseEntity() | ) : BaseEntity() | ||||||
|   | |||||||
| @@ -1,5 +1,48 @@ | |||||||
| package kr.co.vividnext.sodalive.point | package kr.co.vividnext.sodalive.point | ||||||
|  |  | ||||||
|  | import com.querydsl.core.types.dsl.Expressions | ||||||
|  | import com.querydsl.jpa.impl.JPAQueryFactory | ||||||
|  | import kr.co.vividnext.sodalive.content.QAudioContent.audioContent | ||||||
|  | import kr.co.vividnext.sodalive.content.order.QOrder.order | ||||||
|  | import kr.co.vividnext.sodalive.point.QUsePoint.usePoint | ||||||
| import org.springframework.data.jpa.repository.JpaRepository | import org.springframework.data.jpa.repository.JpaRepository | ||||||
|  | import java.time.LocalDateTime | ||||||
|  |  | ||||||
| interface UsePointRepository : JpaRepository<UsePoint, Long> | interface UsePointRepository : JpaRepository<UsePoint, Long>, UsePointQueryRepository | ||||||
|  |  | ||||||
|  | interface UsePointQueryRepository { | ||||||
|  |     fun getPointUseStatusByMemberId(memberId: Long, timezone: String): List<GetPointUseStatusResponse> | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class UsePointQueryRepositoryImpl( | ||||||
|  |     private val queryFactory: JPAQueryFactory | ||||||
|  | ) : UsePointQueryRepository { | ||||||
|  |     override fun getPointUseStatusByMemberId(memberId: Long, timezone: String): List<GetPointUseStatusResponse> { | ||||||
|  |         val formattedDate = Expressions.stringTemplate( | ||||||
|  |             "DATE_FORMAT({0}, {1})", | ||||||
|  |             Expressions.dateTimeTemplate( | ||||||
|  |                 LocalDateTime::class.java, | ||||||
|  |                 "CONVERT_TZ({0},{1},{2})", | ||||||
|  |                 usePoint.createdAt, | ||||||
|  |                 "UTC", | ||||||
|  |                 timezone | ||||||
|  |             ), | ||||||
|  |             "%Y.%m.%d | %H:%i:%s" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return queryFactory | ||||||
|  |             .select( | ||||||
|  |                 QGetPointUseStatusResponse( | ||||||
|  |                     audioContent.title.prepend("[콘텐츠 대여] "), | ||||||
|  |                     formattedDate, | ||||||
|  |                     usePoint.amount | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             .from(usePoint) | ||||||
|  |             .innerJoin(order).on(usePoint.orderId.eq(order.id)) | ||||||
|  |             .innerJoin(audioContent).on(order.audioContent.id.eq(audioContent.id)) | ||||||
|  |             .where(usePoint.memberId.eq(memberId)) | ||||||
|  |             .orderBy(usePoint.id.desc()) | ||||||
|  |             .fetch() | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -2,5 +2,8 @@ package kr.co.vividnext.sodalive.useraction | |||||||
|  |  | ||||||
| enum class ActionType(val displayName: String) { | enum class ActionType(val displayName: String) { | ||||||
|     SIGN_UP("회원가입"), |     SIGN_UP("회원가입"), | ||||||
|     USER_AUTHENTICATION("본인인증") |     USER_AUTHENTICATION("본인인증"), | ||||||
|  |     CONTENT_COMMENT("콘텐츠 댓글"), | ||||||
|  |     ORDER_CONTENT_COMMENT("구매한 콘텐츠 댓글"), | ||||||
|  |     LIVE_CONTINUOUS_LISTEN_30("라이브 연속 청취 30분") | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | package kr.co.vividnext.sodalive.useraction | ||||||
|  |  | ||||||
|  | enum class PolicyType(val displayName: String) { | ||||||
|  |     DAILY("기간 내 매일"), | ||||||
|  |     TOTAL("기간 내 전체") | ||||||
|  | } | ||||||
| @@ -0,0 +1,37 @@ | |||||||
|  | package kr.co.vividnext.sodalive.useraction | ||||||
|  |  | ||||||
|  | import kr.co.vividnext.sodalive.common.ApiResponse | ||||||
|  | import kr.co.vividnext.sodalive.common.SodaException | ||||||
|  | import kr.co.vividnext.sodalive.member.Member | ||||||
|  | import kr.co.vividnext.sodalive.member.MemberService | ||||||
|  | import org.springframework.security.core.annotation.AuthenticationPrincipal | ||||||
|  | import org.springframework.web.bind.annotation.PostMapping | ||||||
|  | import org.springframework.web.bind.annotation.RequestBody | ||||||
|  | import org.springframework.web.bind.annotation.RequestMapping | ||||||
|  | import org.springframework.web.bind.annotation.RestController | ||||||
|  |  | ||||||
|  | @RestController | ||||||
|  | @RequestMapping("/user-action") | ||||||
|  | class UserActionController( | ||||||
|  |     private val service: UserActionService, | ||||||
|  |     private val memberService: MemberService | ||||||
|  | ) { | ||||||
|  |     @PostMapping | ||||||
|  |     fun recordAction( | ||||||
|  |         @RequestBody request: UserActionRequest, | ||||||
|  |         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? | ||||||
|  |     ) = run { | ||||||
|  |         if (member == null) throw SodaException("") | ||||||
|  |  | ||||||
|  |         val memberId = member.id!! | ||||||
|  |         val pushTokenList = memberService.getPushTokenList(recipient = memberId) | ||||||
|  |         service.recordAction( | ||||||
|  |             memberId = memberId, | ||||||
|  |             isAuth = member.auth != null, | ||||||
|  |             actionType = request.actionType, | ||||||
|  |             pushTokenList = pushTokenList | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         ApiResponse.ok(Unit, "") | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -9,5 +9,6 @@ import javax.persistence.Enumerated | |||||||
| data class UserActionLog( | data class UserActionLog( | ||||||
|     val memberId: Long, |     val memberId: Long, | ||||||
|     @Enumerated(EnumType.STRING) |     @Enumerated(EnumType.STRING) | ||||||
|     val actionType: ActionType |     val actionType: ActionType, | ||||||
|  |     val contentCommentId: Long? = null | ||||||
| ) : BaseEntity() | ) : BaseEntity() | ||||||
|   | |||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | package kr.co.vividnext.sodalive.useraction | ||||||
|  |  | ||||||
|  | data class UserActionRequest(val actionType: ActionType) | ||||||
| @@ -3,67 +3,150 @@ package kr.co.vividnext.sodalive.useraction | |||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| import kotlinx.coroutines.Dispatchers | import kotlinx.coroutines.Dispatchers | ||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.launch | ||||||
|  | import kr.co.vividnext.sodalive.content.order.OrderRepository | ||||||
| import kr.co.vividnext.sodalive.fcm.FcmService | import kr.co.vividnext.sodalive.fcm.FcmService | ||||||
| import kr.co.vividnext.sodalive.point.MemberPoint | import kr.co.vividnext.sodalive.point.MemberPoint | ||||||
| import kr.co.vividnext.sodalive.point.MemberPointRepository | import kr.co.vividnext.sodalive.point.MemberPointRepository | ||||||
| import kr.co.vividnext.sodalive.point.PointGrantLog | import kr.co.vividnext.sodalive.point.PointGrantLog | ||||||
| import kr.co.vividnext.sodalive.point.PointGrantLogRepository | import kr.co.vividnext.sodalive.point.PointGrantLogRepository | ||||||
| import kr.co.vividnext.sodalive.point.PointRewardPolicyRepository | import kr.co.vividnext.sodalive.point.PointRewardPolicyRepository | ||||||
|  | import org.slf4j.LoggerFactory | ||||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||||
|  | import org.springframework.transaction.support.TransactionTemplate | ||||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||||
|  |  | ||||||
| @Service | @Service | ||||||
| class UserActionService( | class UserActionService( | ||||||
|     private val repository: UserActionLogRepository, |     private val repository: UserActionLogRepository, | ||||||
|  |     private val orderRepository: OrderRepository, | ||||||
|     private val policyRepository: PointRewardPolicyRepository, |     private val policyRepository: PointRewardPolicyRepository, | ||||||
|     private val grantLogRepository: PointGrantLogRepository, |     private val grantLogRepository: PointGrantLogRepository, | ||||||
|     private val memberPointRepository: MemberPointRepository, |     private val memberPointRepository: MemberPointRepository, | ||||||
|  |     private val transactionTemplate: TransactionTemplate, | ||||||
|  |  | ||||||
|     private val fcmService: FcmService |     private val fcmService: FcmService | ||||||
| ) { | ) { | ||||||
|  |  | ||||||
|     private val coroutineScope = CoroutineScope(Dispatchers.IO) |     private val coroutineScope = CoroutineScope(Dispatchers.IO) | ||||||
|  |  | ||||||
|     fun recordAction(memberId: Long, actionType: ActionType, pushToken: String?) { |     fun recordAction( | ||||||
|  |         memberId: Long, | ||||||
|  |         isAuth: Boolean, | ||||||
|  |         actionType: ActionType, | ||||||
|  |         contentId: Long? = null, | ||||||
|  |         contentCommentId: Long? = null, | ||||||
|  |         pushTokenList: List<String> = emptyList() | ||||||
|  |     ) { | ||||||
|         coroutineScope.launch { |         coroutineScope.launch { | ||||||
|             val now = LocalDateTime.now() |             val now = LocalDateTime.now() | ||||||
|             repository.save(UserActionLog(memberId, actionType)) |             transactionTemplate.execute { | ||||||
|  |                 repository.save( | ||||||
|  |                     UserActionLog( | ||||||
|  |                         memberId = memberId, | ||||||
|  |                         actionType = actionType, | ||||||
|  |                         contentCommentId = contentCommentId | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |                 repository.flush() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (isAuth) { | ||||||
|  |                 try { | ||||||
|  |                     transactionTemplate.execute { | ||||||
|                         val policy = policyRepository.findByActionTypeAndIsActiveTrue(actionType, now) |                         val policy = policyRepository.findByActionTypeAndIsActiveTrue(actionType, now) | ||||||
|                         if (policy != null) { |                         if (policy != null) { | ||||||
|  |                             val policyType = policy.policyType | ||||||
|  |                             val todayAt15 = now.toLocalDate().atTime(15, 0) | ||||||
|  |                             val policyTypeDailyStartDate = if (now.toLocalTime().isBefore(todayAt15.toLocalTime())) { | ||||||
|  |                                 now.toLocalDate().minusDays(1).atTime(15, 0) | ||||||
|  |                             } else { | ||||||
|  |                                 todayAt15 | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             val isValidPolicyTypeDailyAndDailyStartDateAfterPolicyStartDate = | ||||||
|  |                                 policyType == PolicyType.DAILY && policyTypeDailyStartDate >= policy.startDate | ||||||
|  |                             val order = if (contentId != null) { | ||||||
|  |                                 orderRepository.findByMemberIdAndContentId( | ||||||
|  |                                     memberId = memberId, | ||||||
|  |                                     contentId = contentId, | ||||||
|  |                                     createdAt = if (isValidPolicyTypeDailyAndDailyStartDateAfterPolicyStartDate) { | ||||||
|  |                                         policyTypeDailyStartDate | ||||||
|  |                                     } else { | ||||||
|  |                                         policy.startDate | ||||||
|  |                                     } | ||||||
|  |                                 ) | ||||||
|  |                             } else { | ||||||
|  |                                 null | ||||||
|  |                             } | ||||||
|  |                             if (actionType == ActionType.ORDER_CONTENT_COMMENT && order == null) return@execute | ||||||
|  |  | ||||||
|                             val actionCount = repository.countByMemberIdAndActionTypeAndCreatedAtBetween( |                             val actionCount = repository.countByMemberIdAndActionTypeAndCreatedAtBetween( | ||||||
|                                 memberId = memberId, |                                 memberId = memberId, | ||||||
|                                 actionType = actionType, |                                 actionType = actionType, | ||||||
|                     startDate = policy.startDate, |                                 startDate = if (isValidPolicyTypeDailyAndDailyStartDateAfterPolicyStartDate) { | ||||||
|  |                                     policyTypeDailyStartDate | ||||||
|  |                                 } else { | ||||||
|  |                                     policy.startDate | ||||||
|  |                                 }, | ||||||
|                                 endDate = policy.endDate ?: now |                                 endDate = policy.endDate ?: now | ||||||
|                             ) |                             ) | ||||||
|                 if (actionCount < policy.threshold) return@launch |                             if (actionCount < policy.threshold) return@execute | ||||||
|  |  | ||||||
|                 val alreadyGranted = grantLogRepository.existsByMemberIdAndPolicyId(memberId, policy.id!!) |                             val grantedCount = grantLogRepository.countByMemberIdAndPolicyIdAndStartDate( | ||||||
|                 if (alreadyGranted) return@launch |                                 memberId, | ||||||
|  |                                 policy.id!!, | ||||||
|  |                                 startDate = if (isValidPolicyTypeDailyAndDailyStartDateAfterPolicyStartDate) { | ||||||
|  |                                     policyTypeDailyStartDate | ||||||
|  |                                 } else { | ||||||
|  |                                     policy.startDate | ||||||
|  |                                 }, | ||||||
|  |                                 orderId = order?.id | ||||||
|  |                             ) | ||||||
|  |                             if (grantedCount >= policy.availableCount) return@execute | ||||||
|  |  | ||||||
|  |                             val point = if (actionType == ActionType.ORDER_CONTENT_COMMENT && order != null) { | ||||||
|  |                                 order.can | ||||||
|  |                             } else { | ||||||
|  |                                 policy.pointAmount | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             if (point > 0) { | ||||||
|  |                                 grantLogRepository.save( | ||||||
|  |                                     PointGrantLog( | ||||||
|  |                                         memberId = memberId, | ||||||
|  |                                         point = point, | ||||||
|  |                                         actionType = actionType, | ||||||
|  |                                         policyId = policy.id!!, | ||||||
|  |                                         orderId = order?.id | ||||||
|  |                                     ) | ||||||
|  |                                 ) | ||||||
|  |  | ||||||
|                                 memberPointRepository.save( |                                 memberPointRepository.save( | ||||||
|                                     MemberPoint( |                                     MemberPoint( | ||||||
|                                         memberId = memberId, |                                         memberId = memberId, | ||||||
|                         point = policy.pointAmount, |                                         point = point, | ||||||
|                                         actionType = actionType, |                                         actionType = actionType, | ||||||
|                                         expiresAt = now.plusDays(3) |                                         expiresAt = now.plusDays(3) | ||||||
|                                     ) |                                     ) | ||||||
|                                 ) |                                 ) | ||||||
|  |  | ||||||
|                 grantLogRepository.save( |                                 if (pushTokenList.isNotEmpty()) { | ||||||
|                     PointGrantLog( |                                     fcmService.sendPointGranted( | ||||||
|                         memberId = memberId, |                                         pushTokenList, | ||||||
|                         point = policy.pointAmount, |                                         point | ||||||
|                         actionType = actionType, |  | ||||||
|                         policyId = policy.id!! |  | ||||||
|                     ) |  | ||||||
|                                     ) |                                     ) | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } catch (e: Exception) { | ||||||
|  |                     logger.warn("포인트 지급 또는 알림 실패: ${e.message}") | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|                 if (pushToken != null) { |     companion object { | ||||||
|                     fcmService.sendPointGranted(pushToken, policy.pointAmount) |         private val logger = LoggerFactory.getLogger(UserActionService::class.java) | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user