라이브 메인 - 추천라이브, 추천채널, 예약중인 라이브, 진행중인 라이브, 이벤트 배너 API 추가

This commit is contained in:
Klaus 2023-07-27 06:24:23 +09:00
parent ee124e258e
commit ee99dd3147
20 changed files with 757 additions and 0 deletions

View File

@ -0,0 +1,23 @@
package kr.co.vividnext.sodalive.event
import kr.co.vividnext.sodalive.common.BaseEntity
import javax.persistence.Column
import javax.persistence.Entity
@Entity
data class Event(
@Column(nullable = false)
var thumbnailImage: String,
@Column(nullable = true)
var detailImage: String?,
@Column(nullable = true)
var popupImage: String?,
@Column(nullable = true)
var link: String?,
@Column(nullable = true)
var title: String?,
@Column(nullable = false)
var isPopup: Boolean = false,
@Column(nullable = false)
var isActive: Boolean = true
) : BaseEntity()

View File

@ -0,0 +1,13 @@
package kr.co.vividnext.sodalive.event
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/event")
class EventController(private val service: EventService) {
@GetMapping
fun getEventList() = ApiResponse.ok(service.getEventList())
}

View File

@ -0,0 +1,35 @@
package kr.co.vividnext.sodalive.event
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.event.QEvent.event
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface EventRepository : JpaRepository<Event, Long>, EventQueryRepository
interface EventQueryRepository {
fun getEventList(): List<EventItem>
}
@Repository
class EventQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : EventQueryRepository {
override fun getEventList(): List<EventItem> {
return queryFactory
.select(
QEventItem(
event.id,
event.title,
event.thumbnailImage,
event.detailImage,
event.popupImage,
event.link,
event.isPopup
)
)
.from(event)
.where(event.isActive.isTrue)
.orderBy(event.id.desc())
.fetch()
}
}

View File

@ -0,0 +1,35 @@
package kr.co.vividnext.sodalive.event
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
@Service
class EventService(
private val repository: EventRepository,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
fun getEventList(): GetEventResponse {
val eventList = repository.getEventList()
.asSequence()
.map {
if (!it.thumbnailImageUrl.startsWith("https://")) {
it.thumbnailImageUrl = "$cloudFrontHost/${it.thumbnailImageUrl}"
}
if (it.detailImageUrl != null && !it.detailImageUrl!!.startsWith("https://")) {
it.detailImageUrl = "$cloudFrontHost/${it.detailImageUrl}"
}
if (it.popupImageUrl != null && !it.popupImageUrl!!.startsWith("https://")) {
it.popupImageUrl = "$cloudFrontHost/${it.popupImageUrl}"
}
it
}
.toList()
return GetEventResponse(0, eventList)
}
}

View File

@ -0,0 +1,18 @@
package kr.co.vividnext.sodalive.event
import com.querydsl.core.annotations.QueryProjection
data class GetEventResponse(
val totalCount: Int,
val eventList: List<EventItem>
)
data class EventItem @QueryProjection constructor(
val id: Long,
val title: String? = null,
var thumbnailImageUrl: String,
var detailImageUrl: String? = null,
var popupImageUrl: String? = null,
val link: String? = null,
val isPopup: Boolean
)

View File

@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.live.recommend
data class GetRecommendChannelResponse(
val creatorId: Long,
val nickname: String,
val profileImageUrl: String,
val isOnAir: Boolean
)

View File

@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.live.recommend
data class GetRecommendLiveResponse(
val imageUrl: String,
val creatorId: Long
)

View File

@ -0,0 +1,29 @@
package kr.co.vividnext.sodalive.live.recommend
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.RestController
@RestController
class LiveRecommendController(private val service: LiveRecommendService) {
@GetMapping("/live/recommend")
fun getRecommendLive(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(service.getRecommendLive(member))
}
@GetMapping("/live/recommend/channel")
fun getRecommendChannelList(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(service.getRecommendChannelList(member))
}
}

View File

@ -0,0 +1,100 @@
package kr.co.vividnext.sodalive.live.recommend
import com.querydsl.core.types.Projections
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.live.recommend.QRecommendLiveCreatorBanner.recommendLiveCreatorBanner
import kr.co.vividnext.sodalive.live.room.LiveRoomType
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.QMember.member
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
@Repository
class LiveRecommendRepository(private val queryFactory: JPAQueryFactory) {
fun getRecommendLive(memberId: Long, isAdult: Boolean): List<GetRecommendLiveResponse> {
val dateNow = LocalDateTime.now()
var where = recommendLiveCreatorBanner.startDate.loe(dateNow)
.and(recommendLiveCreatorBanner.endDate.goe(dateNow))
if (!isAdult) {
where = where.and(recommendLiveCreatorBanner.isAdult.isFalse)
}
return queryFactory
.select(
Projections.constructor(
GetRecommendLiveResponse::class.java,
recommendLiveCreatorBanner.image,
recommendLiveCreatorBanner.creator.id
)
)
.from(recommendLiveCreatorBanner)
.where(where)
.orderBy(recommendLiveCreatorBanner.orders.asc())
.fetch()
}
fun getOnAirRecommendChannelList(
memberId: Long,
isAdult: Boolean
): List<GetRecommendChannelResponse> {
var where = member.role.eq(MemberRole.CREATOR)
.and(member.isActive.isTrue)
if (!isAdult) {
where = where.and(liveRoom.isAdult.isFalse)
}
return queryFactory
.select(
Projections.constructor(
GetRecommendChannelResponse::class.java,
member.id,
member.nickname,
member.profileImage,
Expressions.asBoolean(true)
)
)
.from(liveRoom)
.rightJoin(liveRoom.member, member)
.where(
where
.and(liveRoom.isActive.isTrue)
.and(liveRoom.type.ne(LiveRoomType.SECRET))
.and(liveRoom.channelName.isNotNull)
.and(liveRoom.channelName.isNotEmpty)
)
.groupBy(member.id)
.orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc())
.limit(20)
.fetch()
}
fun getRecommendChannelList(
memberId: Long,
withOutCreatorList: List<Long>,
limit: Long
): List<GetRecommendChannelResponse> {
val where = member.role.eq(MemberRole.CREATOR)
.and(member.isActive.isTrue)
return queryFactory
.select(
Projections.constructor(
GetRecommendChannelResponse::class.java,
member.id,
member.nickname,
member.profileImage,
Expressions.asBoolean(false)
)
)
.from(member)
.where(where.and(member.id.notIn(withOutCreatorList)))
.orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc())
.limit(limit)
.fetch()
}
}

View File

@ -0,0 +1,35 @@
package kr.co.vividnext.sodalive.live.recommend
import kr.co.vividnext.sodalive.member.Member
import org.springframework.stereotype.Service
@Service
class LiveRecommendService(private val repository: LiveRecommendRepository) {
fun getRecommendLive(member: Member): List<GetRecommendLiveResponse> {
return repository.getRecommendLive(
memberId = member.id!!,
isAdult = member.auth != null
)
}
fun getRecommendChannelList(member: Member): List<GetRecommendChannelResponse> {
val onAirChannelList = repository.getOnAirRecommendChannelList(member.id!!, isAdult = member.auth != null)
if (onAirChannelList.size >= 20) {
return onAirChannelList
}
val onAirCreatorIdList = onAirChannelList.asSequence()
.map { it.creatorId }
.toList()
val notOnAirCreatorList = repository.getRecommendChannelList(
member.id!!,
withOutCreatorList = onAirCreatorIdList,
limit = (20 - onAirChannelList.size).toLong()
)
return onAirChannelList + notOnAirCreatorList
}
}

View File

@ -0,0 +1,28 @@
package kr.co.vividnext.sodalive.live.recommend
import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.member.Member
import java.time.LocalDateTime
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.JoinColumn
import javax.persistence.ManyToOne
@Entity
data class RecommendLiveCreatorBanner(
@Column(nullable = false)
var image: String,
@Column(nullable = false)
var startDate: LocalDateTime,
@Column(nullable = false)
var endDate: LocalDateTime,
@Column(nullable = false)
var isAdult: Boolean = false,
@Column(nullable = false)
var orders: Int = 1
) : BaseEntity() {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "creator_id", nullable = false)
var creator: Member? = null
}

View File

@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.live.room
data class GetRoomListResponse(
val roomId: Long,
val title: String,
val content: String,
val beginDateTime: String,
val numberOfParticipate: Int,
val numberOfPeople: Int,
val coverImageUrl: String,
val isAdult: Boolean,
val price: Int,
val tags: List<String>,
val channelName: String?,
val managerNickname: String,
val managerId: Long,
val isReservation: Boolean,
val isPrivateRoom: Boolean
)

View File

@ -0,0 +1,51 @@
package kr.co.vividnext.sodalive.live.room
import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.member.Member
import java.time.LocalDateTime
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.EnumType
import javax.persistence.Enumerated
import javax.persistence.FetchType
import javax.persistence.JoinColumn
import javax.persistence.OneToOne
@Entity
data class LiveRoom(
var title: String,
@Column(columnDefinition = "TEXT", nullable = false)
var notice: String,
var beginDateTime: LocalDateTime,
var numberOfPeople: Int,
var coverImage: String? = null,
var bgImage: String? = null,
var isAdult: Boolean,
val price: Int = 0,
@Enumerated(value = EnumType.STRING)
val type: LiveRoomType = LiveRoomType.OPEN,
@Column(nullable = true)
var password: String? = null
) : BaseEntity() {
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
var member: Member? = null
var channelName: String? = null
var isActive: Boolean = true
}
enum class LiveRoomType {
// 공개
OPEN,
// 비공개
PRIVATE,
// 비밀방
SECRET
}
enum class LiveRoomStatus {
NOW, RESERVATION
}

View File

@ -0,0 +1,29 @@
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.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.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/live/room")
class LiveRoomController(private val service: LiveRoomService) {
@GetMapping
fun getRoomList(
@RequestParam timezone: String,
@RequestParam dateString: String? = null,
@RequestParam status: LiveRoomStatus,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
pageable: Pageable
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(service.getRoomList(dateString, status, pageable, member, timezone))
}
}

View File

@ -0,0 +1,119 @@
package kr.co.vividnext.sodalive.live.room
import com.querydsl.core.types.OrderSpecifier
import com.querydsl.core.types.Predicate
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.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.QMember.member
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@Repository
interface LiveRoomRepository : JpaRepository<LiveRoom, Long>, LiveRoomQueryRepository
interface LiveRoomQueryRepository {
fun getLiveRoomList(
dateString: String?,
status: LiveRoomStatus,
pageable: Pageable,
member: Member,
timezone: String,
isAdult: Boolean
): List<LiveRoom>
}
class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveRoomQueryRepository {
override fun getLiveRoomList(
dateString: String?,
status: LiveRoomStatus,
pageable: Pageable,
member: Member,
timezone: String,
isAdult: Boolean
): List<LiveRoom> {
var where: Predicate
if (status == LiveRoomStatus.NOW) {
where = liveRoom.channelName.isNotNull
.and(liveRoom.channelName.isNotEmpty)
} else {
where = if (dateString != null) {
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val date = LocalDate.parse(dateString, dateTimeFormatter).atStartOfDay()
.atZone(ZoneId.of(timezone))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
liveRoom.beginDateTime.goe(date)
.and(liveRoom.beginDateTime.lt(date.plusDays(1)))
.and(
liveRoom.channelName.isNull
.or(liveRoom.channelName.isEmpty)
)
} else {
liveRoom.beginDateTime.gt(
LocalDateTime.now()
.atZone(ZoneId.of(timezone))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
)
.and(
liveRoom.channelName.isNull
.or(liveRoom.channelName.isEmpty)
)
}
}
if (!isAdult) {
where = where.and(liveRoom.isAdult.isFalse)
}
where = where.and(liveRoom.isActive.isTrue)
.and(liveRoom.member.isNotNull)
.and(liveRoom.type.ne(LiveRoomType.SECRET))
return queryFactory
.selectFrom(liveRoom)
.offset(pageable.offset)
.limit(pageable.pageSize.toLong())
.where(where)
.orderBy(
*orderByFieldAccountId(
memberId = member.id!!,
status = status,
offset = pageable.offset,
dateString = dateString
)
)
.fetch()
}
private fun orderByFieldAccountId(
memberId: Long,
status: LiveRoomStatus,
offset: Long,
dateString: String?
): Array<out OrderSpecifier<*>> {
return if (status == LiveRoomStatus.NOW) {
arrayOf(Expressions.numberTemplate(Double::class.java, "function('rand')").asc())
} else if (status == LiveRoomStatus.RESERVATION && offset == 0L && dateString == null) {
arrayOf(
CaseBuilder()
.`when`(member.id.eq(memberId)).then(1)
.otherwise(2)
.asc(),
liveRoom.beginDateTime.asc()
)
} else {
arrayOf(liveRoom.beginDateTime.asc())
}
}
}

View File

@ -0,0 +1,72 @@
package kr.co.vividnext.sodalive.live.room
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository
import kr.co.vividnext.sodalive.member.Member
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.domain.Pageable
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@Service
class LiveRoomService(
private val repository: LiveRoomRepository,
private val roomInfoRepository: LiveRoomInfoRedisRepository,
@Value("\${cloud.aws.cloud-front.host}")
private val coverImageHost: String
) {
fun getRoomList(
dateString: String?,
status: LiveRoomStatus,
pageable: Pageable,
member: Member,
timezone: String
): List<GetRoomListResponse> {
return repository
.getLiveRoomList(
dateString = dateString,
status = status,
pageable = pageable,
member = member,
timezone = timezone,
isAdult = member.auth != null
)
.asSequence()
.map {
val roomInfo = roomInfoRepository.findByIdOrNull(it.id!!)
val beginDateTime = it.beginDateTime
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of(timezone))
GetRoomListResponse(
roomId = it.id!!,
title = it.title,
content = it.notice,
beginDateTime = beginDateTime.format(
DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")
),
numberOfParticipate = (roomInfo?.listenerCount ?: 0) +
(roomInfo?.speakerCount ?: 0) +
(roomInfo?.managerCount ?: 0),
numberOfPeople = it.numberOfPeople,
isAdult = it.isAdult,
price = it.price,
channelName = it.channelName,
managerNickname = it.member!!.nickname,
managerId = it.member!!.id!!,
tags = listOf(),
coverImageUrl = if (it.coverImage!!.startsWith("https://")) {
it.coverImage!!
} else {
"$coverImageHost/${it.coverImage!!}"
},
isReservation = false,
isPrivateRoom = it.type == LiveRoomType.PRIVATE
)
}
.toList()
}
}

View File

@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.live.room.donation
import java.util.UUID
data class LiveRoomDonationMessage(
val uuid: String = UUID.randomUUID().toString(),
val nickname: String,
val coinMessage: String,
val donationMessage: String
)

View File

@ -0,0 +1,101 @@
package kr.co.vividnext.sodalive.live.room.info
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessage
import kr.co.vividnext.sodalive.member.Member
import org.springframework.data.annotation.Id
import org.springframework.data.redis.core.RedisHash
@RedisHash("live_room_info")
data class LiveRoomInfo(
@Id
val roomId: Long,
var speakerList: List<LiveRoomMember> = mutableListOf(),
var listenerList: List<LiveRoomMember> = mutableListOf(),
var managerList: List<LiveRoomMember> = mutableListOf(),
var donationMessageList: List<LiveRoomDonationMessage> = mutableListOf()
) {
var speakerCount = 0
private set
var listenerCount = 0
private set
var managerCount = 0
private set
fun addSpeaker(member: Member) {
val liveRoomMember = LiveRoomMember(member)
liveRoomMember.role = LiveRoomMemberRole.SPEAKER
val speakerSet = speakerList.toMutableSet()
speakerSet.add(liveRoomMember)
speakerList = speakerSet.toList()
setSpeakerCount()
}
fun removeSpeaker(member: Member) {
(speakerList as MutableList).removeIf { it.id == member.id!! }
setSpeakerCount()
}
private fun setSpeakerCount() {
speakerCount = speakerList.size
}
fun addListener(member: Member) {
val liveRoomMember = LiveRoomMember(member)
liveRoomMember.role = LiveRoomMemberRole.LISTENER
val listenerSet = listenerList.toMutableSet()
listenerSet.add(liveRoomMember)
listenerList = listenerSet.toList()
setListenerCount()
}
fun removeListener(member: Member) {
(listenerList as MutableList).removeIf { it.id == member.id!! }
setListenerCount()
}
private fun setListenerCount() {
listenerCount = listenerList.size
}
fun addManager(member: Member) {
val liveRoomMember = LiveRoomMember(member)
liveRoomMember.role = LiveRoomMemberRole.MANAGER
val managerSet = managerList.toMutableSet()
managerSet.add(liveRoomMember)
managerList = managerSet.toList()
setManagerCount()
}
fun removeManager(member: Member) {
(managerList as MutableList).removeIf { it.id == member.id!! }
setManagerCount()
}
private fun setManagerCount() {
managerCount = managerList.size
}
fun addDonationMessage(nickname: String, coin: Int, donationMessage: String) {
val donationMessageSet = donationMessageList.toMutableSet()
donationMessageSet.add(
LiveRoomDonationMessage(
nickname = nickname,
coinMessage = "${coin}코인을 후원하셨습니다.",
donationMessage = donationMessage
)
)
donationMessageList = donationMessageSet.toList()
}
fun removeDonationMessage(uuid: String) {
(donationMessageList as MutableList).removeIf { it.uuid == uuid }
}
}

View File

@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.live.room.info
import org.springframework.data.repository.CrudRepository
interface LiveRoomInfoRedisRepository : CrudRepository<LiveRoomInfo, Long>

View File

@ -0,0 +1,21 @@
package kr.co.vividnext.sodalive.live.room.info
import kr.co.vividnext.sodalive.member.Member
data class LiveRoomMember(
val id: Long,
val nickname: String,
val profileImage: String
) {
var role = LiveRoomMemberRole.LISTENER
constructor(member: Member) : this(
id = member.id!!,
nickname = member.nickname,
profileImage = member.profileImage ?: "profile/default-profile.png"
)
}
enum class LiveRoomMemberRole {
LISTENER, SPEAKER, MANAGER
}