From f393c7630e37b62bb81977b04ae23070fe8e3b96 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 31 Jul 2023 17:09:45 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20-=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91,=20=EC=B7=A8=EC=86=8C,=20=EC=9E=85=EC=9E=A5,=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=98=88=EC=95=BD=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vividnext/sodalive/can/CanRepository.kt | 16 ++ .../sodalive/can/charge/ChargeRepository.kt | 73 +++++- .../sodalive/can/payment/CanPaymentService.kt | 245 ++++++++++++++++++ .../sodalive/can/use/TotalSpentCan.kt | 18 ++ .../sodalive/can/use/UseCanCalculate.kt | 2 + .../can/use/UseCanCalculateRepository.kt | 12 + .../sodalive/can/use/UseCanRepository.kt | 7 + .../reservation/LiveReservationController.kt | 24 ++ .../reservation/LiveReservationRepository.kt | 70 +++++ .../reservation/LiveReservationService.kt | 79 ++++++ .../reservation/MakeLiveReservationRequest.kt | 8 + .../MakeLiveReservationResponse.kt | 12 + .../live/room/EditLiveRoomInfoRequest.kt | 9 + .../live/room/EnterOrQuitLiveRoomRequest.kt | 7 + .../live/room/GetRecentRoomInfoResponse.kt | 9 + .../vividnext/sodalive/live/room/LiveRoom.kt | 7 + .../sodalive/live/room/LiveRoomController.kt | 62 ++++- .../sodalive/live/room/LiveRoomRepository.kt | 36 ++- .../sodalive/live/room/LiveRoomService.kt | 225 +++++++++++++++- .../sodalive/live/room/StartLiveRequest.kt | 8 + .../live/room/cancel/CancelLiveRequest.kt | 3 + .../live/room/cancel/LiveRoomCancel.kt | 21 ++ .../room/cancel/LiveRoomCancelRepository.kt | 7 + .../live/room/detail/GetRoomDetailResponse.kt | 2 +- .../visit/GetRecentVisitRoomMemberResponse.kt | 7 + .../sodalive/live/room/visit/LiveRoomVisit.kt | 24 ++ .../room/visit/LiveRoomVisitRepository.kt | 41 +++ .../live/room/visit/LiveRoomVisitService.kt | 25 ++ .../kr/co/vividnext/sodalive/member/Member.kt | 4 +- 29 files changed, 1044 insertions(+), 19 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/use/TotalSpentCan.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculateRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/EditLiveRoomInfoRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRecentRoomInfoResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/StartLiveRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/CancelLiveRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/LiveRoomCancel.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/LiveRoomCancelRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/GetRecentVisitRoomMemberResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisit.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt index 6c2f964..a404594 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt @@ -27,6 +27,7 @@ interface CanQueryRepository { fun getCanUseStatus(member: Member, pageable: Pageable): List fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List fun isExistPaidLiveRoom(memberId: Long, roomId: Long): UseCan? + fun getCanUsedForLiveRoomNotRefund(memberId: Long, roomId: Long): UseCan? } @Repository @@ -111,4 +112,19 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue .orderBy(useCan.id.desc()) .fetchFirst() } + + override fun getCanUsedForLiveRoomNotRefund(memberId: Long, roomId: Long): UseCan? { + return queryFactory + .selectFrom(useCan) + .innerJoin(useCan.member, member) + .innerJoin(useCan.room, liveRoom) + .where( + member.id.eq(memberId) + .and(liveRoom.id.eq(roomId)) + .and(useCan.canUsage.eq(CanUsage.LIVE)) + .and(useCan.isRefund.isFalse) + ) + .orderBy(useCan.id.desc()) + .fetchFirst() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt index d9d02d6..d8d5bf4 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt @@ -1,7 +1,78 @@ package kr.co.vividnext.sodalive.can.charge +import com.querydsl.core.types.dsl.BooleanExpression +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.can.charge.QCharge.charge +import kr.co.vividnext.sodalive.can.payment.PaymentGateway +import kr.co.vividnext.sodalive.can.payment.PaymentStatus +import kr.co.vividnext.sodalive.can.payment.QPayment.payment +import kr.co.vividnext.sodalive.member.QMember.member import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @Repository -interface ChargeRepository : JpaRepository +interface ChargeRepository : JpaRepository, ChargeQueryRepository + +interface ChargeQueryRepository { + fun getOldestChargeWhereRewardCanGreaterThan0(chargeId: Long, memberId: Long, container: String): Charge? + fun getOldestChargeWhereChargeCanGreaterThan0(chargeId: Long, memberId: Long, container: String): Charge? +} + +class ChargeQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : ChargeQueryRepository { + override fun getOldestChargeWhereRewardCanGreaterThan0( + chargeId: Long, + memberId: Long, + container: String + ): Charge? { + return queryFactory + .selectFrom(charge) + .innerJoin(charge.member, member) + .leftJoin(charge.payment, payment) + .where( + member.id.eq(memberId) + .and(charge.rewardCan.gt(0)) + .and(charge.id.gt(chargeId)) + .and(payment.status.eq(PaymentStatus.COMPLETE)) + .and(getPaymentGatewayCondition(container)) + ) + .orderBy(charge.id.asc()) + .fetchFirst() + } + + override fun getOldestChargeWhereChargeCanGreaterThan0( + chargeId: Long, + memberId: Long, + container: String + ): Charge? { + return queryFactory + .selectFrom(charge) + .innerJoin(charge.member, member) + .leftJoin(charge.payment, payment) + .where( + member.id.eq(memberId) + .and(charge.chargeCan.gt(0)) + .and(charge.id.gt(chargeId)) + .and(payment.status.eq(PaymentStatus.COMPLETE)) + .and(getPaymentGatewayCondition(container)) + ) + .orderBy(charge.id.asc()) + .fetchFirst() + } + + private fun getPaymentGatewayCondition(container: String): BooleanExpression? { + val paymentGatewayCondition = when (container) { + "aos" -> { + payment.paymentGateway.eq(PaymentGateway.PG) + .or(payment.paymentGateway.eq(PaymentGateway.GOOGLE_IAP)) + } + + "ios" -> { + payment.paymentGateway.eq(PaymentGateway.PG) + .or(payment.paymentGateway.eq(PaymentGateway.APPLE_IAP)) + } + + else -> payment.paymentGateway.eq(PaymentGateway.PG) + } + return paymentGatewayCondition + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt new file mode 100644 index 0000000..434ced3 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt @@ -0,0 +1,245 @@ +package kr.co.vividnext.sodalive.can.payment + +import kr.co.vividnext.sodalive.can.charge.ChargeRepository +import kr.co.vividnext.sodalive.can.use.CanUsage +import kr.co.vividnext.sodalive.can.use.SpentCan +import kr.co.vividnext.sodalive.can.use.TotalSpentCan +import kr.co.vividnext.sodalive.can.use.UseCan +import kr.co.vividnext.sodalive.can.use.UseCanCalculate +import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository +import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus +import kr.co.vividnext.sodalive.can.use.UseCanRepository +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.live.room.LiveRoom +import kr.co.vividnext.sodalive.member.MemberRepository +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class CanPaymentService( + private val memberRepository: MemberRepository, + private val chargeRepository: ChargeRepository, + private val useCanRepository: UseCanRepository, + private val useCanCalculateRepository: UseCanCalculateRepository +) { + @Transactional + fun spendCan( + memberId: Long, + needCan: Int, + canUsage: CanUsage, + liveRoom: LiveRoom? = null, + container: String + ) { + val member = memberRepository.findByIdOrNull(id = memberId) + ?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") + val useRewardCan = spendRewardCan(memberId, needCan, container) + val useChargeCan = if (needCan - useRewardCan.total > 0) { + spendChargeCan(memberId, needCan = needCan - useRewardCan.total, container = container) + } else { + null + } + + if (needCan - useRewardCan.total - (useChargeCan?.total ?: 0) > 0) { + throw SodaException( + "${needCan - useRewardCan.total - (useChargeCan?.total ?: 0)} " + + "캔이 부족합니다. 충전 후 이용해 주세요." + ) + } + + if (!useRewardCan.verify() || useChargeCan?.verify() == false) { + throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") + } + + val useCan = UseCan( + canUsage = canUsage, + can = useChargeCan?.total ?: 0, + rewardCan = useRewardCan.total + ) + + var recipientId: Long? = null + if (canUsage == CanUsage.LIVE && liveRoom != null) { + recipientId = liveRoom.member!!.id!! + useCan.room = liveRoom + useCan.member = member + } else if (canUsage == CanUsage.CHANGE_NICKNAME) { + useCan.member = member + } else if (canUsage == CanUsage.DONATION && liveRoom != null) { + recipientId = liveRoom.member!!.id!! + useCan.room = liveRoom + useCan.member = member + } else { + throw SodaException("잘못된 요청입니다.") + } + + useCanRepository.save(useCan) + + setUseCoinCalculate(recipientId, useRewardCan, useChargeCan, useCan, paymentGateway = PaymentGateway.PG) + setUseCoinCalculate( + recipientId, + useRewardCan, + useChargeCan, + useCan, + paymentGateway = PaymentGateway.GOOGLE_IAP + ) + setUseCoinCalculate( + recipientId, + useRewardCan, + useChargeCan, + useCan, + paymentGateway = PaymentGateway.APPLE_IAP + ) + } + + private fun setUseCoinCalculate( + recipientId: Long?, + useRewardCan: TotalSpentCan, + useChargeCan: TotalSpentCan?, + useCan: UseCan, + paymentGateway: PaymentGateway + ) { + val totalSpentRewardCan = useRewardCan.spentCans + .filter { it.paymentGateway == paymentGateway } + .fold(0) { sum, spentCans -> sum + spentCans.can } + + val useCanCalculate = if (useChargeCan != null) { + val totalSpentChargeCan = useChargeCan.spentCans + .filter { it.paymentGateway == paymentGateway } + .fold(0) { sum, spentCans -> sum + spentCans.can } + + UseCanCalculate( + can = totalSpentChargeCan + totalSpentRewardCan, + paymentGateway = paymentGateway, + status = UseCanCalculateStatus.RECEIVED + ) + } else { + UseCanCalculate( + can = totalSpentRewardCan, + paymentGateway = paymentGateway, + status = UseCanCalculateStatus.RECEIVED + ) + } + + if (useCanCalculate.can > 0) { + useCanCalculate.useCan = useCan + useCanCalculate.recipientCreatorId = recipientId + useCanCalculateRepository.save(useCanCalculate) + } + } + + private fun spendRewardCan(memberId: Long, needCan: Int, container: String): TotalSpentCan { + return if (needCan > 0) { + val member = memberRepository.findByIdOrNull(id = memberId) + ?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") + + val spentCans = mutableListOf() + var chargeId = 0L + var total = 0 + + while (needCan - total > 0) { + val remainingNeedCan = needCan - total + val charge = chargeRepository.getOldestChargeWhereRewardCanGreaterThan0(chargeId, memberId, container) + ?: break + + if (charge.rewardCan >= remainingNeedCan) { + charge.rewardCan -= remainingNeedCan + + when (charge.payment!!.paymentGateway) { + PaymentGateway.PG -> member.pgRewardCan -= remainingNeedCan + PaymentGateway.APPLE_IAP -> member.appleRewardCan -= remainingNeedCan + PaymentGateway.GOOGLE_IAP -> member.pgRewardCan -= remainingNeedCan + } + + total += remainingNeedCan + + spentCans.add( + SpentCan( + paymentGateway = charge.payment!!.paymentGateway, + can = remainingNeedCan + ) + ) + } else { + total += charge.rewardCan + spentCans.add( + SpentCan( + paymentGateway = charge.payment!!.paymentGateway, + can = charge.rewardCan + ) + ) + + when (charge.payment!!.paymentGateway) { + PaymentGateway.PG -> member.pgRewardCan -= remainingNeedCan + PaymentGateway.APPLE_IAP -> member.appleRewardCan -= remainingNeedCan + PaymentGateway.GOOGLE_IAP -> member.pgRewardCan -= remainingNeedCan + } + + charge.rewardCan = 0 + } + + chargeId = charge.id!! + } + + TotalSpentCan(spentCans, total) + } else { + TotalSpentCan(total = 0) + } + } + + private fun spendChargeCan(memberId: Long, needCan: Int, container: String): TotalSpentCan { + return if (needCan > 0) { + val member = memberRepository.findByIdOrNull(id = memberId) + ?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") + + val spentCans = mutableListOf() + var chargeId = 0L + var total = 0 + + while (needCan - total > 0) { + val remainingNeedCan = needCan - total + val charge = chargeRepository.getOldestChargeWhereChargeCanGreaterThan0(chargeId, memberId, container) + ?: break + + if (charge.rewardCan >= remainingNeedCan) { + charge.rewardCan -= remainingNeedCan + + when (charge.payment!!.paymentGateway) { + PaymentGateway.PG -> member.pgRewardCan -= remainingNeedCan + PaymentGateway.APPLE_IAP -> member.appleRewardCan -= remainingNeedCan + PaymentGateway.GOOGLE_IAP -> member.pgRewardCan -= remainingNeedCan + } + + total += remainingNeedCan + + spentCans.add( + SpentCan( + paymentGateway = charge.payment!!.paymentGateway, + can = remainingNeedCan + ) + ) + } else { + total += charge.rewardCan + spentCans.add( + SpentCan( + paymentGateway = charge.payment!!.paymentGateway, + can = charge.rewardCan + ) + ) + + when (charge.payment!!.paymentGateway) { + PaymentGateway.PG -> member.pgRewardCan -= remainingNeedCan + PaymentGateway.APPLE_IAP -> member.appleRewardCan -= remainingNeedCan + PaymentGateway.GOOGLE_IAP -> member.pgRewardCan -= remainingNeedCan + } + + charge.rewardCan = 0 + } + + chargeId = charge.id!! + } + + TotalSpentCan(spentCans, total) + } else { + TotalSpentCan(total = 0) + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/TotalSpentCan.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/TotalSpentCan.kt new file mode 100644 index 0000000..7b3f283 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/TotalSpentCan.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.can.use + +import kr.co.vividnext.sodalive.can.payment.PaymentGateway + +data class TotalSpentCan( + val spentCans: MutableList = mutableListOf(), + val total: Int +) { + fun verify(): Boolean { + val sumSpentCans = spentCans.fold(0) { sum, spentCan -> sum + spentCan.can } + return total == sumSpentCans + } +} + +data class SpentCan( + val paymentGateway: PaymentGateway, + val can: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculate.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculate.kt index 0982736..3b0efbe 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculate.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculate.kt @@ -30,6 +30,8 @@ data class UseCanCalculate( value?.useCanCalculates?.add(this) field = value } + + var recipientCreatorId: Long? = null } enum class UseCanCalculateStatus { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculateRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculateRepository.kt new file mode 100644 index 0000000..3d47f56 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculateRepository.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.can.use + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface UseCanCalculateRepository : JpaRepository { + fun findByUseCanIdAndStatus( + useCanId: Long, + status: UseCanCalculateStatus = UseCanCalculateStatus.RECEIVED + ): List +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanRepository.kt new file mode 100644 index 0000000..f30429a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.can.use + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface UseCanRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationController.kt new file mode 100644 index 0000000..a0abd2c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationController.kt @@ -0,0 +1,24 @@ +package kr.co.vividnext.sodalive.live.reservation + +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.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/live/reservation") +class LiveReservationController(private val service: LiveReservationService) { + @PostMapping + fun makeReservation( + @RequestBody request: MakeLiveReservationRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.makeReservation(request, member.id!!)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationRepository.kt new file mode 100644 index 0000000..ecaf7d2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationRepository.kt @@ -0,0 +1,70 @@ +package kr.co.vividnext.sodalive.live.reservation + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.live.reservation.QLiveReservation.liveReservation +import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface LiveReservationRepository : JpaRepository, LiveReservationQueryRepository + +interface LiveReservationQueryRepository { + fun getReservationList(roomId: Long): List + + fun cancelReservation(roomId: Long) + + fun getReservationBookerList(roomId: Long): List + + fun isExistsReservation(roomId: Long, memberId: Long): Boolean +} + +@Repository +class LiveReservationQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveReservationQueryRepository { + override fun getReservationList(roomId: Long): List { + return queryFactory + .selectFrom(liveReservation) + .innerJoin(liveReservation.room, liveRoom) + .where( + liveRoom.id.eq(roomId) + .and(liveReservation.isActive.isTrue) + ) + .fetch() + } + + override fun cancelReservation(roomId: Long) { + queryFactory + .update(liveReservation) + .set(liveReservation.isActive, false) + .where(liveReservation.room.id.eq(roomId)) + .execute() + } + + override fun getReservationBookerList(roomId: Long): List { + return queryFactory + .select(member) + .from(liveReservation) + .innerJoin(liveReservation.member, member) + .innerJoin(liveReservation.room, liveRoom) + .where( + liveRoom.id.eq(roomId) + .and(liveReservation.isActive.isTrue) + ) + .fetch() + } + + override fun isExistsReservation(roomId: Long, memberId: Long): Boolean { + return queryFactory + .selectFrom(liveReservation) + .innerJoin(liveReservation.member, member) + .innerJoin(liveReservation.room, liveRoom) + .where( + liveReservation.isActive.isTrue + .and(liveReservation.room.id.eq(roomId)) + .and(liveReservation.member.id.eq(memberId)) + ) + .fetchFirst() != null + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationService.kt new file mode 100644 index 0000000..9184f72 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationService.kt @@ -0,0 +1,79 @@ +package kr.co.vividnext.sodalive.live.reservation + +import kr.co.vividnext.sodalive.can.payment.CanPaymentService +import kr.co.vividnext.sodalive.can.use.CanUsage +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.live.room.LiveRoomRepository +import kr.co.vividnext.sodalive.live.room.LiveRoomType +import kr.co.vividnext.sodalive.member.MemberRepository +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Service +class LiveReservationService( + private val repository: LiveReservationRepository, + private val liveRoomRepository: LiveRoomRepository, + private val memberRepository: MemberRepository, + private val canPaymentService: CanPaymentService +) { + fun makeReservation(request: MakeLiveReservationRequest, memberId: Long): MakeLiveReservationResponse { + val room = liveRoomRepository.findByIdOrNull(id = request.roomId) + ?: throw SodaException(message = "잘못된 요청입니다.\n다시 시도해 주세요.") + + val member = memberRepository.findByIdOrNull(id = memberId) + ?: throw SodaException(message = "로그인 정보를 확인해주세요.") + + if ( + room.member!!.id!! != memberId && + room.type == LiveRoomType.PRIVATE && + (request.password == null || request.password != room.password) + ) { + throw SodaException("비밀번호가 일치하지 않습니다.\n다시 확인 후 입력해주세요.") + } + + if (repository.isExistsReservation(roomId = request.roomId, memberId = memberId)) { + throw SodaException("이미 예약한 라이브 입니다.") + } + + val haveCan = member.getChargeCan(request.container) + member.getRewardCan(request.container) + if (haveCan < room.price) { + throw SodaException("${room.price - haveCan}캔이 부족합니다. 충전 후 이용해 주세요.") + } + + if (room.price > 0) { + canPaymentService.spendCan( + memberId = member.id!!, + needCan = room.price, + canUsage = CanUsage.LIVE, + liveRoom = room, + container = request.container + ) + } + + val reservation = LiveReservation() + reservation.room = room + reservation.member = member + repository.save(reservation) + + val beginDateTime = room.beginDateTime + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(request.timezone)) + + return MakeLiveReservationResponse( + reservationId = reservation.id!!, + nickname = room.member!!.nickname, + title = room.title, + beginDateString = beginDateTime.format(DateTimeFormatter.ofPattern("yyyy년 M월 d일 (E), a hh:mm")), + price = if (room.price > 0) { + "${room.price} 캔" + } else { + "무료" + }, + haveCan = haveCan, + useCan = room.price, + remainingCan = haveCan - room.price + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationRequest.kt new file mode 100644 index 0000000..01d00a8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.live.reservation + +data class MakeLiveReservationRequest( + val roomId: Long, + val container: String, + val timezone: String = "Asia/Seoul", + val password: String? = null +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationResponse.kt new file mode 100644 index 0000000..3cd4ff0 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationResponse.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.live.reservation + +data class MakeLiveReservationResponse( + val reservationId: Long, + val nickname: String, + val title: String, + val beginDateString: String, + val price: String, + val haveCan: Int, + val useCan: Int, + val remainingCan: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/EditLiveRoomInfoRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/EditLiveRoomInfoRequest.kt new file mode 100644 index 0000000..437e8f9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/EditLiveRoomInfoRequest.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.live.room + +data class EditLiveRoomInfoRequest( + val title: String?, + val notice: String?, + val numberOfPeople: Int?, + val beginDateTimeString: String?, + val timezone: String? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt new file mode 100644 index 0000000..223c7e8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.live.room + +data class EnterOrQuitLiveRoomRequest( + val roomId: Long, + val container: String, + val password: String? = null +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRecentRoomInfoResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRecentRoomInfoResponse.kt new file mode 100644 index 0000000..4a076d2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRecentRoomInfoResponse.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.live.room + +data class GetRecentRoomInfoResponse( + val title: String, + val notice: String, + var coverImageUrl: String, + val coverImagePath: String, + val numberOfPeople: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt index 85035a3..3c95441 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt @@ -3,6 +3,8 @@ package kr.co.vividnext.sodalive.live.room import kr.co.vividnext.sodalive.can.use.UseCan import kr.co.vividnext.sodalive.common.BaseEntity import kr.co.vividnext.sodalive.live.reservation.LiveReservation +import kr.co.vividnext.sodalive.live.room.cancel.LiveRoomCancel +import kr.co.vividnext.sodalive.live.room.visit.LiveRoomVisit import kr.co.vividnext.sodalive.member.Member import java.time.LocalDateTime import javax.persistence.CascadeType @@ -44,6 +46,11 @@ data class LiveRoom( @OneToMany(mappedBy = "room", fetch = FetchType.LAZY) var reservations: MutableList = mutableListOf() + @OneToMany(mappedBy = "room", fetch = FetchType.LAZY) + var visits: MutableList = mutableListOf() + + @OneToOne(mappedBy = "room") + var cancel: LiveRoomCancel? = null var channelName: String? = null var isActive: Boolean = true } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt index c15d880..9ff9e48 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt @@ -2,12 +2,15 @@ package kr.co.vividnext.sodalive.live.room import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.live.room.cancel.CancelLiveRequest import kr.co.vividnext.sodalive.member.Member import org.springframework.data.domain.Pageable 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.RequestPart @@ -48,10 +51,59 @@ class LiveRoomController(private val service: LiveRoomService) { @RequestParam timezone: String, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member != null) { - ApiResponse.ok(service.getRoomDetail(id, member, timezone)) - } else { - throw SodaException("로그인 정보를 확인해주세요.") - } + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getRoomDetail(id, member, timezone)) + } + + @PostMapping("/enter") + fun enterLive( + @RequestBody request: EnterOrQuitLiveRoomRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.enterLive(request, member)) + } + + @PutMapping("/start") + fun startLive( + @RequestBody request: StartLiveRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.startLive(request, member)) + } + + @PutMapping("/cancel") + fun cancelLive( + @RequestBody request: CancelLiveRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.cancelLive(request, member)) + } + + @GetMapping("/recent-room-info") + fun getRecentRoomInfo( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getRecentRoomInfo(member)) + } + + @PutMapping("/{id}") + fun editLiveRoomInfo( + @PathVariable("id") roomId: Long, + @RequestPart("coverImage") coverImage: MultipartFile?, + @RequestPart("request") requestString: String?, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.editLiveRoomInfo(roomId, coverImage, requestString, member)) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt index 169ae95..f9f0873 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt @@ -2,11 +2,10 @@ package kr.co.vividnext.sodalive.live.room import com.querydsl.core.types.OrderSpecifier import com.querydsl.core.types.Predicate +import com.querydsl.core.types.Projections import com.querydsl.core.types.dsl.CaseBuilder import com.querydsl.core.types.dsl.Expressions import com.querydsl.jpa.impl.JPAQueryFactory -import kr.co.vividnext.sodalive.live.reservation.LiveReservation -import kr.co.vividnext.sodalive.live.reservation.QLiveReservation.liveReservation import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.QMember @@ -33,7 +32,8 @@ interface LiveRoomQueryRepository { ): List fun getLiveRoom(id: Long): LiveRoom? - fun getReservationList(roomId: Long): List + fun getLiveRoomAndAccountId(roomId: Long, memberId: Long): LiveRoom? + fun getRecentRoomInfo(memberId: Long, cloudFrontHost: String): GetRecentRoomInfoResponse? } class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveRoomQueryRepository { @@ -112,15 +112,35 @@ class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : L .fetchFirst() } - override fun getReservationList(roomId: Long): List { + override fun getLiveRoomAndAccountId(roomId: Long, memberId: Long): LiveRoom? { return queryFactory - .selectFrom(liveReservation) - .innerJoin(liveReservation.room, liveRoom) + .selectFrom(liveRoom) + .innerJoin(liveRoom.member, member) .where( liveRoom.id.eq(roomId) - .and(liveReservation.isActive.isTrue) + .and(liveRoom.isActive.isTrue) + .and(member.id.eq(memberId)) ) - .fetch() + .fetchFirst() + } + + override fun getRecentRoomInfo(memberId: Long, cloudFrontHost: String): GetRecentRoomInfoResponse? { + return queryFactory + .select( + Projections.constructor( + GetRecentRoomInfoResponse::class.java, + liveRoom.title, + liveRoom.notice, + liveRoom.coverImage.prepend("/").prepend(cloudFrontHost), + liveRoom.coverImage, + liveRoom.numberOfPeople + ) + ) + .from(liveRoom) + .where(liveRoom.member.id.eq(memberId)) + .orderBy(liveRoom.id.desc()) + .limit(1) + .fetchFirst() } private fun orderByFieldAccountId( 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 6a6e191..032a5a6 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 @@ -4,14 +4,31 @@ import com.amazonaws.services.s3.model.ObjectMetadata import com.fasterxml.jackson.databind.ObjectMapper import kr.co.vividnext.sodalive.aws.s3.S3Uploader 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.extensions.convertLocalDateTime +import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository +import kr.co.vividnext.sodalive.live.room.cancel.CancelLiveRequest +import kr.co.vividnext.sodalive.live.room.cancel.LiveRoomCancel +import kr.co.vividnext.sodalive.live.room.cancel.LiveRoomCancelRepository import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailManager import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser +import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfo import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository +import kr.co.vividnext.sodalive.live.room.visit.LiveRoomVisitService import kr.co.vividnext.sodalive.live.tag.LiveTagRepository import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value import org.springframework.data.domain.Pageable @@ -28,7 +45,14 @@ import java.time.format.DateTimeFormatter class LiveRoomService( private val repository: LiveRoomRepository, private val roomInfoRepository: LiveRoomInfoRedisRepository, + private val roomCancelRepository: LiveRoomCancelRepository, + private val useCanCalculateRepository: UseCanCalculateRepository, + private val reservationRepository: LiveReservationRepository, + private val roomVisitService: LiveRoomVisitService, + private val canPaymentService: CanPaymentService, + private val chargeRepository: ChargeRepository, + private val memberRepository: MemberRepository, private val tagRepository: LiveTagRepository, private val canRepository: CanRepository, private val objectMapper: ObjectMapper, @@ -194,7 +218,7 @@ class LiveRoomService( val response = GetRoomDetailResponse( roomId = roomId, title = room.title, - content = room.notice, + notice = room.notice, price = room.price, tags = room.tags.asSequence().filter { it.tag.isActive }.map { it.tag.tag }.toList(), numberOfParticipantsTotal = room.numberOfPeople, @@ -254,7 +278,7 @@ class LiveRoomService( response.numberOfParticipants = users.size } } else { - val reservationList = repository.getReservationList(roomId) + val reservationList = reservationRepository.getReservationList(roomId) response.participatingUsers = reservationList .asSequence() .map { @@ -270,4 +294,201 @@ class LiveRoomService( return response } + + @Transactional + fun startLive(request: StartLiveRequest, member: Member) { + val room = repository.getLiveRoomAndAccountId(request.roomId, member.id!!) + ?: throw SodaException("해당하는 라이브가 없습니다.") + + val dateTime = LocalDateTime.now() + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(request.timezone)) + .toLocalDateTime() + + val beginDateTime = room.beginDateTime + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(request.timezone)) + .toLocalDateTime() + + if (dateTime.plusMinutes(10).isBefore(beginDateTime)) { + val startAvailableDateTimeString = beginDateTime.minusMinutes(10).format( + DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a") + ) + throw SodaException("$startAvailableDateTimeString 이후에 시작할 수 있습니다.") + } + + room.channelName = "SODA_LIVE_CHANNEL_" + + "${member.id}_${dateTime.year}_${dateTime.month}_${dateTime.dayOfMonth}_" + + "${dateTime.hour}_${dateTime.minute}" + } + + fun cancelLive(request: CancelLiveRequest, member: Member) { + val room = repository.getLiveRoomAndAccountId(request.roomId, member.id!!) + ?: throw SodaException("해당하는 라이브가 없습니다.") + + if (request.reason.isBlank()) { + throw SodaException("취소사유를 입력해 주세요.") + } + room.isActive = false + + val roomCancel = LiveRoomCancel(request.reason) + roomCancel.room = room + roomCancelRepository.save(roomCancel) + + // 유료방인 경우 환불처리 + if (room.price > 0) { + val bookerList = reservationRepository.getReservationBookerList(roomId = room.id!!) + for (booker in bookerList) { + val useCan = canRepository.getCanUsedForLiveRoomNotRefund(memberId = booker.id!!, roomId = room.id!!) + ?: continue + useCan.isRefund = true + + val useCanCalculate = useCanCalculateRepository.findByUseCanIdAndStatus(useCanId = useCan.id!!) + useCanCalculate.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.PG -> booker.pgRewardCan += charge.rewardCan + PaymentGateway.GOOGLE_IAP -> booker.googleRewardCan += charge.rewardCan + PaymentGateway.APPLE_IAP -> booker.appleRewardCan += charge.rewardCan + } + charge.member = booker + + val payment = Payment( + status = PaymentStatus.COMPLETE, + paymentGateway = it.paymentGateway + ) + payment.method = "환불" + charge.payment = payment + + chargeRepository.save(charge) + } + } + } + + reservationRepository.cancelReservation(roomId = room.id!!) + } + + fun enterLive(request: EnterOrQuitLiveRoomRequest, member: Member) { + val room = repository.getLiveRoom(id = request.roomId) + ?: throw SodaException("해당하는 라이브가 없습니다.") + + if ( + room.member!!.id!! != member.id!! && + room.type == LiveRoomType.PRIVATE && + (request.password == null || request.password != room.password) + ) { + throw SodaException("비밀번호가 일치하지 않습니다.\n다시 확인 후 입력해주세요.") + } + + var roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) + if (roomInfo == null) { + roomInfo = roomInfoRepository.save(LiveRoomInfo(roomId = request.roomId)) + } + + if (roomInfo.speakerCount + roomInfo.listenerCount + roomInfo.managerCount >= room.numberOfPeople) { + throw SodaException("방이 가득찼습니다.") + } + + if ( + room.price > 0 && + room.member!!.id!! != member.id!! && + canRepository.isExistPaidLiveRoom(memberId = member.id!!, roomId = request.roomId) == null + ) { + val findMember = memberRepository.findByIdOrNull(id = member.id!!) + ?: throw SodaException("로그인 정보를 확인해 주세요.") + + val totalCan = findMember.getChargeCan(request.container) + findMember.getRewardCan(request.container) + if (totalCan < room.price) { + throw SodaException("${room.price - totalCan}캔이 부족합니다. 충전 후 이용해 주세요.") + } + + canPaymentService.spendCan( + memberId = member.id!!, + needCan = room.price, + canUsage = CanUsage.LIVE, + liveRoom = room, + container = request.container + ) + } + + roomInfo.removeListener(member) + roomInfo.removeSpeaker(member) + roomInfo.removeManager(member) + + if (room.member!!.id == member.id) { + roomInfo.addSpeaker(member) + } else { + roomInfo.addListener(member) + } + + roomInfoRepository.save(roomInfo) + roomVisitService.roomVisit(room, member) + } + + fun getRecentRoomInfo(member: Member): GetRecentRoomInfoResponse { + return repository.getRecentRoomInfo(memberId = member.id!!, cloudFrontHost = cloudFrontHost) + ?: throw SodaException("최근 데이터가 없습니다.") + } + + @Transactional + fun editLiveRoomInfo(roomId: Long, coverImage: MultipartFile?, requestString: String?, member: Member) { + val room = repository.getLiveRoom(roomId) + if (member.id == null || room?.member?.id != member.id!!) { + throw SodaException("잘못된 요청입니다.") + } + + if (coverImage == null && requestString == null) { + throw SodaException("변경사항이 없습니다.") + } + + if (requestString != null) { + val request = objectMapper.readValue(requestString, EditLiveRoomInfoRequest::class.java) + + if (request.title != null) { + room.title = request.title + } + + if (request.notice != null) { + room.notice = request.notice + } + + if (request.numberOfPeople != null) { + room.numberOfPeople = request.numberOfPeople + } + + if (request.beginDateTimeString != null && request.timezone != null) { + room.beginDateTime = request.beginDateTimeString.convertLocalDateTime("yyyy-MM-dd HH:mm") + .atZone(ZoneId.of(request.timezone)) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + } + } + + if (coverImage != null) { + val metadata = ObjectMetadata() + metadata.contentLength = coverImage.size + + // 커버 이미지 파일명 생성 + val coverImageFileName = generateFileName(prefix = "${room.id}-cover") + + // 커버 이미지 업로드 + val coverImagePath = s3Uploader.upload( + inputStream = coverImage.inputStream, + bucket = coverImageBucket, + filePath = "suda_room_cover/${room.id}/$coverImageFileName", + metadata = metadata + ) + + room.bgImage = coverImagePath + + if (room.channelName == null) { + room.coverImage = coverImagePath + } + } + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/StartLiveRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/StartLiveRequest.kt new file mode 100644 index 0000000..b59570f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/StartLiveRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.live.room + +import java.util.TimeZone + +data class StartLiveRequest( + val roomId: Long, + val timezone: String = TimeZone.getDefault().id +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/CancelLiveRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/CancelLiveRequest.kt new file mode 100644 index 0000000..d6add40 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/CancelLiveRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.live.room.cancel + +data class CancelLiveRequest(val roomId: Long, val reason: String) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/LiveRoomCancel.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/LiveRoomCancel.kt new file mode 100644 index 0000000..e4bc12c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/LiveRoomCancel.kt @@ -0,0 +1,21 @@ +package kr.co.vividnext.sodalive.live.room.cancel + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.live.room.LiveRoom +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.OneToOne + +@Entity +data class LiveRoomCancel( + val reason: String +) : BaseEntity() { + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + var room: LiveRoom? = null + set(value) { + value?.cancel = this + field = value + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/LiveRoomCancelRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/LiveRoomCancelRepository.kt new file mode 100644 index 0000000..959a77d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/LiveRoomCancelRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.live.room.cancel + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface LiveRoomCancelRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt index 8a8a0d8..be95f68 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt @@ -7,7 +7,7 @@ data class GetRoomDetailResponse( val roomId: Long, val price: Int, val title: String, - val content: String, + val notice: String, var isPaid: Boolean, val isPrivateRoom: Boolean, val password: String?, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/GetRecentVisitRoomMemberResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/GetRecentVisitRoomMemberResponse.kt new file mode 100644 index 0000000..a5dc76c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/GetRecentVisitRoomMemberResponse.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.live.room.visit + +data class GetRecentVisitRoomMemberResponse( + val userId: Long, + val nickname: String, + val profileImageUrl: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisit.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisit.kt new file mode 100644 index 0000000..622a585 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisit.kt @@ -0,0 +1,24 @@ +package kr.co.vividnext.sodalive.live.room.visit + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.live.room.LiveRoom +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +class LiveRoomVisit : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + var room: LiveRoom? = null + set(value) { + value?.visits?.add(this) + field = value + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt new file mode 100644 index 0000000..b90ce96 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt @@ -0,0 +1,41 @@ +package kr.co.vividnext.sodalive.live.room.visit + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom +import kr.co.vividnext.sodalive.live.room.visit.QLiveRoomVisit.liveRoomVisit +import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface LiveRoomVisitRepository : JpaRepository, LiveRoomVisitQueryRepository + +interface LiveRoomVisitQueryRepository { + fun findByRoomIdAndMemberId(roomId: Long, memberId: Long): LiveRoomVisit? + fun findFirstByMemberIdOrderByUpdatedAtDesc(memberId: Long): LiveRoomVisit? +} + +@Repository +class LiveRoomVisitQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveRoomVisitQueryRepository { + override fun findByRoomIdAndMemberId(roomId: Long, memberId: Long): LiveRoomVisit? { + return queryFactory + .selectFrom(liveRoomVisit) + .innerJoin(liveRoomVisit.room, liveRoom) + .innerJoin(liveRoomVisit.member, member) + .where( + liveRoom.id.eq(roomId) + .and(member.id.eq(memberId)) + ) + .orderBy(liveRoomVisit.id.desc()) + .fetchFirst() + } + + override fun findFirstByMemberIdOrderByUpdatedAtDesc(memberId: Long): LiveRoomVisit? { + return queryFactory + .selectFrom(liveRoomVisit) + .innerJoin(liveRoomVisit.room, liveRoom) + .where(member.id.eq(memberId)) + .orderBy(liveRoomVisit.updatedAt.desc()) + .fetchFirst() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt new file mode 100644 index 0000000..df18ba1 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt @@ -0,0 +1,25 @@ +package kr.co.vividnext.sodalive.live.room.visit + +import kr.co.vividnext.sodalive.live.room.LiveRoom +import kr.co.vividnext.sodalive.member.Member +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +@Service +@Transactional(readOnly = true) +class LiveRoomVisitService(private val repository: LiveRoomVisitRepository) { + @Transactional + fun roomVisit(room: LiveRoom, member: Member) { + var roomVisit = repository.findByRoomIdAndMemberId(room.id!!, member.id!!) + if (roomVisit == null) { + roomVisit = LiveRoomVisit() + roomVisit.member = member + roomVisit.room = room + } else { + roomVisit.updatedAt = LocalDateTime.now() + } + + repository.save(roomVisit) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt index a500c93..b8badb2 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt @@ -53,9 +53,9 @@ data class Member( // 화폐 private var pgChargeCan: Int = 0 - private var pgRewardCan: Int = 0 + var pgRewardCan: Int = 0 private var googleChargeCan: Int = 0 - private var googleRewardCan: Int = 0 + var googleRewardCan: Int = 0 var appleChargeCan: Int = 0 var appleRewardCan: Int = 0