diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt index 5b0ec53..09989bd 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt @@ -7,6 +7,8 @@ import org.springframework.context.annotation.Configuration import org.springframework.data.redis.cache.RedisCacheConfiguration import org.springframework.data.redis.cache.RedisCacheManager import org.springframework.data.redis.connection.RedisConnectionFactory +import org.springframework.data.redis.connection.RedisStandaloneConfiguration +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory import org.springframework.data.redis.core.RedisTemplate import org.springframework.data.redis.repository.configuration.EnableRedisRepositories @@ -26,7 +28,12 @@ class RedisConfig( ) { @Bean fun redisConnectionFactory(): RedisConnectionFactory { - return LettuceConnectionFactory(host, port) + val clientConfiguration = LettuceClientConfiguration.builder() + .useSsl() + .disablePeerVerification() + .build() + + return LettuceConnectionFactory(RedisStandaloneConfiguration(host, port), clientConfiguration) } @Bean diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 25ab32f..7fb751c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -39,7 +39,7 @@ import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository import kr.co.vividnext.sodalive.live.room.info.LiveRoomMember import kr.co.vividnext.sodalive.live.room.kickout.LiveRoomKickOutService import kr.co.vividnext.sodalive.live.room.visit.LiveRoomVisitService -import kr.co.vividnext.sodalive.live.roulette.RouletteRepository +import kr.co.vividnext.sodalive.live.roulette.NewRouletteRepository import kr.co.vividnext.sodalive.live.tag.LiveTagRepository import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.member.Member @@ -66,7 +66,7 @@ import kotlin.concurrent.write @Transactional(readOnly = true) class LiveRoomService( private val repository: LiveRoomRepository, - private val rouletteRepository: RouletteRepository, + private val rouletteRepository: NewRouletteRepository, private val roomInfoRepository: LiveRoomInfoRedisRepository, private val roomCancelRepository: LiveRoomCancelRepository, private val kickOutService: LiveRoomKickOutService, @@ -679,7 +679,15 @@ class LiveRoomService( .getNotificationUserIds(room.member!!.id!!) .contains(member.id) - val isActiveRoulette = rouletteRepository.findByIdOrNull(room.member!!.id!!)?.isActive ?: false + var isActiveRoulette = false + val rouletteList = rouletteRepository.findByCreatorId(creatorId = room.member!!.id!!) + + for (roulette in rouletteList) { + if (roulette.isActive) { + isActiveRoulette = true + break + } + } val donationRankingTop3UserIds = if (room.member!!.isVisibleDonationRank) { explorerQueryRepository @@ -1004,10 +1012,12 @@ class LiveRoomService( kickOutService.deleteKickOutData(roomId = room.id!!) roomInfoRepository.deleteById(roomInfo.roomId) - val roulette = rouletteRepository.findByIdOrNull(member.id!!) - if (roulette != null) { - roulette.isActive = false - rouletteRepository.save(roulette) + val rouletteList = rouletteRepository.findByCreatorId(creatorId = member.id!!) + if (rouletteList.isNotEmpty()) { + rouletteList.forEach { + it.isActive = false + rouletteRepository.save(it) + } } } else { roomInfo.removeSpeaker(member) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/CreateNewRouletteRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/CreateNewRouletteRequest.kt new file mode 100644 index 0000000..979a025 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/CreateNewRouletteRequest.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.live.roulette + +data class CreateNewRouletteRequest( + val can: Int, + val isActive: Boolean, + val items: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/GetNewRouletteResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/GetNewRouletteResponse.kt new file mode 100644 index 0000000..2dcb511 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/GetNewRouletteResponse.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.live.roulette + +data class GetNewRouletteResponse( + val id: Long, + val can: Int, + val isActive: Boolean, + val items: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/NewRoulette.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/NewRoulette.kt new file mode 100644 index 0000000..262bf40 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/NewRoulette.kt @@ -0,0 +1,16 @@ +package kr.co.vividnext.sodalive.live.roulette + +import org.springframework.data.redis.core.RedisHash +import org.springframework.data.redis.core.index.Indexed +import javax.persistence.Id + +@RedisHash("newRoulette") +data class NewRoulette( + @Id + val id: Long, + @Indexed + val creatorId: Long, + var can: Int, + var isActive: Boolean, + var items: List = mutableListOf() +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/NewRouletteController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/NewRouletteController.kt new file mode 100644 index 0000000..9464285 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/NewRouletteController.kt @@ -0,0 +1,87 @@ +package kr.co.vividnext.sodalive.live.roulette + +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.MemberRole +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/new-roulette") +class NewRouletteController(private val service: NewRouletteService) { + @GetMapping("/creator") + @PreAuthorize("hasRole('CREATOR')") + fun getAllRoulette( + @RequestParam creatorId: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getAllRoulette(creatorId = creatorId, memberId = member.id!!)) + } + + @PostMapping + @PreAuthorize("hasRole('CREATOR')") + fun createNewRoulette( + @RequestBody request: CreateNewRouletteRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null || member.role != MemberRole.CREATOR) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + ApiResponse.ok(service.createRoulette(memberId = member.id!!, request = request)) + } + + @PutMapping + @PreAuthorize("hasRole('CREATOR')") + fun updateNewRoulette( + @RequestBody request: UpdateNewRouletteRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null || member.role != MemberRole.CREATOR) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + ApiResponse.ok(service.updateRoulette(memberId = member.id!!, request = request)) + } + + @GetMapping + fun getRoulette( + @RequestParam creatorId: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ): ApiResponse { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + return ApiResponse.ok(service.getRoulette(creatorId = creatorId, memberId = member.id!!)) + } + + @PostMapping("/spin") + fun spinRoulette( + @RequestBody request: SpinRouletteRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.spinRoulette(request = request, memberId = member.id!!)) + } + + @PostMapping("/refund/{id}") + fun refundDonation( + @PathVariable id: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.refundDonation(id, member)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/NewRouletteRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/NewRouletteRepository.kt new file mode 100644 index 0000000..f2043be --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/NewRouletteRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.live.roulette + +import org.springframework.data.repository.CrudRepository + +interface NewRouletteRepository : CrudRepository { + fun findByCreatorId(creatorId: Long): List +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/NewRouletteService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/NewRouletteService.kt new file mode 100644 index 0000000..fa3ea40 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/NewRouletteService.kt @@ -0,0 +1,246 @@ +package kr.co.vividnext.sodalive.live.roulette + +import kr.co.vividnext.sodalive.can.CanRepository +import kr.co.vividnext.sodalive.can.charge.Charge +import kr.co.vividnext.sodalive.can.charge.ChargeRepository +import kr.co.vividnext.sodalive.can.charge.ChargeStatus +import kr.co.vividnext.sodalive.can.payment.CanPaymentService +import kr.co.vividnext.sodalive.can.payment.Payment +import kr.co.vividnext.sodalive.can.payment.PaymentGateway +import kr.co.vividnext.sodalive.can.payment.PaymentStatus +import kr.co.vividnext.sodalive.can.use.CanUsage +import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository +import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.live.room.LiveRoomRepository +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRepository +import kr.co.vividnext.sodalive.member.MemberRole +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class NewRouletteService( + private val idGenerator: RedisIdGenerator, + private val canPaymentService: CanPaymentService, + + private val canRepository: CanRepository, + private val repository: NewRouletteRepository, + private val oldRepository: RouletteRepository, + private val roomRepository: LiveRoomRepository, + private val memberRepository: MemberRepository, + private val chargeRepository: ChargeRepository, + private val useCanCalculateRepository: UseCanCalculateRepository +) { + fun getAllRoulette(creatorId: Long, memberId: Long): List { + if (creatorId != memberId) throw SodaException("잘못된 요청입니다.") + + var rouletteList = repository.findByCreatorId(creatorId) + if (rouletteList.isEmpty()) { + val roulette = oldRepository.findByIdOrNull(creatorId) + if (roulette != null) { + repository.save( + NewRoulette( + id = idGenerator.generateId(SEQUENCE_NAME), + creatorId = creatorId, + can = roulette.can, + isActive = false, + items = roulette.items + ) + ) + + rouletteList = repository.findByCreatorId(creatorId) + oldRepository.deleteById(roulette.creatorId) + } + } + rouletteList.sortedBy { it.id } + + return rouletteList.asSequence() + .map { + GetNewRouletteResponse( + it.id, + it.can, + it.isActive, + it.items + ) + } + .toList() + } + + fun createRoulette(memberId: Long, request: CreateNewRouletteRequest): Boolean { + rouletteValidate(can = request.can, items = request.items) + + if (request.isActive) { + val rouletteList = repository.findByCreatorId(creatorId = memberId) + rouletteList.forEach { + it.isActive = false + repository.save(it) + } + } + + val roulette = NewRoulette( + id = idGenerator.generateId(SEQUENCE_NAME), + creatorId = memberId, + can = request.can, + isActive = request.isActive, + items = request.items + ) + + repository.save(roulette) + return request.isActive + } + + fun updateRoulette(memberId: Long, request: UpdateNewRouletteRequest): Boolean { + rouletteValidate(can = request.can, items = request.items) + + val rouletteList = repository.findByCreatorId(creatorId = memberId) + + if (rouletteList.isEmpty()) { + throw SodaException("잘못된 요청입니다.") + } + + var isActive = false + rouletteList.forEach { + if (request.isActive || it.isActive) { + isActive = true + } + + if (it.id == request.id) { + it.can = request.can + it.items = request.items + it.isActive = request.isActive + repository.save(it) + } else if (request.isActive) { + it.isActive = false + repository.save(it) + } + } + + return isActive + } + + fun getRoulette(creatorId: Long, memberId: Long): GetRouletteResponse { + val rouletteList = repository.findByCreatorId(creatorId = creatorId) + + if (rouletteList.isEmpty()) { + throw SodaException("룰렛을 사용할 수 없습니다.") + } + + var activeRoulette: NewRoulette? = null + for (roulette in rouletteList) { + if (roulette.isActive) { + activeRoulette = roulette + break + } + } + + if (activeRoulette == null || activeRoulette.items.isEmpty()) { + throw SodaException("룰렛을 사용할 수 없습니다.") + } + + return GetRouletteResponse( + can = activeRoulette.can, + isActive = activeRoulette.isActive, + items = activeRoulette.items + ) + } + + @Transactional + fun spinRoulette(request: SpinRouletteRequest, memberId: Long): GetRouletteResponse { + // STEP 1 - 라이브 정보 가져오기 + val room = roomRepository.findByIdOrNull(request.roomId) + ?: throw SodaException("해당하는 라이브가 없습니다.") + + val host = room.member ?: throw SodaException("잘못된 요청입니다.") + + if (host.role != MemberRole.CREATOR) { + throw SodaException("비비드넥스트와 계약한\n크리에이터의 룰렛만 사용하실 수 있습니다.") + } + + // STEP 2 - 룰렛 데이터 가져오기 + val rouletteList = repository.findByCreatorId(creatorId = host.id!!) + + if (rouletteList.isEmpty()) { + throw SodaException("룰렛을 사용할 수 없습니다.") + } + + var activeRoulette: NewRoulette? = null + for (roulette in rouletteList) { + if (roulette.isActive) { + activeRoulette = roulette + break + } + } + + if (activeRoulette == null || activeRoulette.items.isEmpty()) { + throw SodaException("룰렛을 사용할 수 없습니다.") + } + + // STEP 3 - 캔 사용 + canPaymentService.spendCan( + memberId = memberId, + needCan = activeRoulette.can, + canUsage = CanUsage.SPIN_ROULETTE, + liveRoom = room, + container = request.container + ) + + return GetRouletteResponse( + can = activeRoulette.can, + isActive = activeRoulette.isActive, + items = activeRoulette.items + ) + } + + @Transactional + fun refundDonation(roomId: Long, member: Member) { + val donator = memberRepository.findByIdOrNull(member.id) + ?: throw SodaException("룰렛 돌리기에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요.") + + val useCan = canRepository.getCanUsedForLiveRoomNotRefund( + memberId = member.id!!, + roomId = roomId, + canUsage = CanUsage.SPIN_ROULETTE + ) ?: throw SodaException("룰렛 돌리기에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요.") + useCan.isRefund = true + + val useCanCalculates = useCanCalculateRepository.findByUseCanIdAndStatus(useCan.id!!) + useCanCalculates.forEach { + it.status = UseCanCalculateStatus.REFUND + val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE) + charge.title = "${it.can} 캔" + charge.useCan = useCan + + when (it.paymentGateway) { + PaymentGateway.GOOGLE_IAP -> donator.googleRewardCan += charge.rewardCan + PaymentGateway.APPLE_IAP -> donator.appleRewardCan += charge.rewardCan + else -> donator.pgRewardCan += charge.rewardCan + } + charge.member = donator + + val payment = Payment( + status = PaymentStatus.COMPLETE, + paymentGateway = it.paymentGateway + ) + payment.method = "룰렛 환불" + charge.payment = payment + + chargeRepository.save(charge) + } + } + + private fun rouletteValidate(can: Int, items: List) { + if (can < 5) { + throw SodaException("룰렛 금액은 최소 5캔 입니다.") + } + + if (items.size < 2 || items.size > 10) { + throw SodaException("룰렛 옵션은 최소 2개, 최대 10개까지 설정할 수 있습니다.") + } + } + + companion object { + const val SEQUENCE_NAME = "newRoulette:sequence" + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/RedisIdGenerator.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/RedisIdGenerator.kt new file mode 100644 index 0000000..f6a31e1 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/RedisIdGenerator.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.live.roulette + +import org.springframework.data.redis.core.StringRedisTemplate +import org.springframework.stereotype.Service + +@Service +class RedisIdGenerator(private val stringRedisTemplate: StringRedisTemplate) { + fun generateId(key: String): Long { + return stringRedisTemplate.opsForValue().increment(key, 1) ?: 1L + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/RouletteService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/RouletteService.kt index 82d9039..d3826e4 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/RouletteService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/RouletteService.kt @@ -86,7 +86,7 @@ class RouletteService( val host = room.member ?: throw SodaException("잘못된 요청입니다.") if (host.role != MemberRole.CREATOR) { - throw SodaException("비비드넥스트와 계약한\n크리에이터에게만 후원을 하실 수 있습니다.") + throw SodaException("비비드넥스트와 계약한\n크리에이터의 룰렛만 사용하실 수 있습니다.") } // STEP 2 - 룰렛 데이터 가져오기 @@ -138,7 +138,7 @@ class RouletteService( status = PaymentStatus.COMPLETE, paymentGateway = it.paymentGateway ) - payment.method = "환불" + payment.method = "룰렛 환불" charge.payment = payment chargeRepository.save(charge) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/UpdateNewRouletteRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/UpdateNewRouletteRequest.kt new file mode 100644 index 0000000..e2752f7 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/roulette/UpdateNewRouletteRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.live.roulette + +data class UpdateNewRouletteRequest( + val id: Long, + val can: Int, + val isActive: Boolean, + val items: List +)