From ee99dd31479b87d7c75dea3784be877296c9d47a Mon Sep 17 00:00:00 2001 From: Klaus Date: Thu, 27 Jul 2023 06:24:23 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20-=20=EC=B6=94=EC=B2=9C=EB=9D=BC=EC=9D=B4=EB=B8=8C,?= =?UTF-8?q?=20=EC=B6=94=EC=B2=9C=EC=B1=84=EB=84=90,=20=EC=98=88=EC=95=BD?= =?UTF-8?q?=EC=A4=91=EC=9D=B8=20=EB=9D=BC=EC=9D=B4=EB=B8=8C,=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=EC=A4=91=EC=9D=B8=20=EB=9D=BC=EC=9D=B4=EB=B8=8C,=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=B0=EB=84=88=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/event/Event.kt | 23 ++++ .../sodalive/event/EventController.kt | 13 ++ .../sodalive/event/EventRepository.kt | 35 ++++++ .../vividnext/sodalive/event/EventService.kt | 35 ++++++ .../sodalive/event/GetEventResponse.kt | 18 +++ .../recommend/GetRecommendChannelResponse.kt | 8 ++ .../recommend/GetRecommendLiveResponse.kt | 6 + .../live/recommend/LiveRecommendController.kt | 29 +++++ .../live/recommend/LiveRecommendRepository.kt | 100 +++++++++++++++ .../live/recommend/LiveRecommendService.kt | 35 ++++++ .../recommend/RecommendLiveCreatorBanner.kt | 28 +++++ .../sodalive/live/room/GetRoomListResponse.kt | 19 +++ .../vividnext/sodalive/live/room/LiveRoom.kt | 51 ++++++++ .../sodalive/live/room/LiveRoomController.kt | 29 +++++ .../sodalive/live/room/LiveRoomRepository.kt | 119 ++++++++++++++++++ .../sodalive/live/room/LiveRoomService.kt | 72 +++++++++++ .../room/donation/LiveRoomDonationMessage.kt | 10 ++ .../sodalive/live/room/info/LiveRoomInfo.kt | 101 +++++++++++++++ .../room/info/LiveRoomInfoRedisRepository.kt | 5 + .../sodalive/live/room/info/LiveRoomMember.kt | 21 ++++ 20 files changed, 757 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/event/Event.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/event/EventController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/event/EventRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/event/EventService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/event/GetEventResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetRecommendChannelResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetRecommendLiveResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBanner.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRoomListResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfoRedisRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/event/Event.kt b/src/main/kotlin/kr/co/vividnext/sodalive/event/Event.kt new file mode 100644 index 0000000..f529a46 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/event/Event.kt @@ -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() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/event/EventController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventController.kt new file mode 100644 index 0000000..fdbe64b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventController.kt @@ -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()) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/event/EventRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventRepository.kt new file mode 100644 index 0000000..9b45e2f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventRepository.kt @@ -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, EventQueryRepository + +interface EventQueryRepository { + fun getEventList(): List +} + +@Repository +class EventQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : EventQueryRepository { + override fun getEventList(): List { + 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() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/event/EventService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventService.kt new file mode 100644 index 0000000..0469524 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventService.kt @@ -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) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/event/GetEventResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/event/GetEventResponse.kt new file mode 100644 index 0000000..5a0b6ee --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/event/GetEventResponse.kt @@ -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 +) + +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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetRecommendChannelResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetRecommendChannelResponse.kt new file mode 100644 index 0000000..587673d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetRecommendChannelResponse.kt @@ -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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetRecommendLiveResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetRecommendLiveResponse.kt new file mode 100644 index 0000000..97f21fe --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetRecommendLiveResponse.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.live.recommend + +data class GetRecommendLiveResponse( + val imageUrl: String, + val creatorId: Long +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt new file mode 100644 index 0000000..3c38eef --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt @@ -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)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt new file mode 100644 index 0000000..7ebacf8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt @@ -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 { + 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 { + 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, + limit: Long + ): List { + 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() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt new file mode 100644 index 0000000..9b400ae --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt @@ -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 { + return repository.getRecommendLive( + memberId = member.id!!, + isAdult = member.auth != null + ) + } + + fun getRecommendChannelList(member: Member): List { + 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 + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBanner.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBanner.kt new file mode 100644 index 0000000..78e823e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBanner.kt @@ -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 +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRoomListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRoomListResponse.kt new file mode 100644 index 0000000..84f8796 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRoomListResponse.kt @@ -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, + val channelName: String?, + val managerNickname: String, + val managerId: Long, + val isReservation: Boolean, + val isPrivateRoom: Boolean +) 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 new file mode 100644 index 0000000..2578439 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt @@ -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 +} 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 new file mode 100644 index 0000000..ec927e4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt @@ -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)) + } +} 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 new file mode 100644 index 0000000..d3ec2d2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt @@ -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, LiveRoomQueryRepository + +interface LiveRoomQueryRepository { + fun getLiveRoomList( + dateString: String?, + status: LiveRoomStatus, + pageable: Pageable, + member: Member, + timezone: String, + isAdult: Boolean + ): List +} + +class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveRoomQueryRepository { + override fun getLiveRoomList( + dateString: String?, + status: LiveRoomStatus, + pageable: Pageable, + member: Member, + timezone: String, + isAdult: Boolean + ): List { + 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> { + 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()) + } + } +} 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 new file mode 100644 index 0000000..24a41b8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -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 { + 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() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt new file mode 100644 index 0000000..4655c93 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt @@ -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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt new file mode 100644 index 0000000..d666ae2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt @@ -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 = mutableListOf(), + var listenerList: List = mutableListOf(), + var managerList: List = mutableListOf(), + var donationMessageList: List = 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 } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfoRedisRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfoRedisRepository.kt new file mode 100644 index 0000000..a39c911 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfoRedisRepository.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.live.room.info + +import org.springframework.data.repository.CrudRepository + +interface LiveRoomInfoRedisRepository : CrudRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt new file mode 100644 index 0000000..cdca980 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt @@ -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 +}